# Stripe setup

Xylo CMS bills through **the business owner’s own Stripe account**. There is no Stripe Connect and no Xylo platform fee. Money does not pass through Xylo.

> **Caution.** This is for the owner and their agent together. Live keys take real money. Use test keys until the Stripe account is verified and a bank is connected. Do not put secret keys in site config or in the Git repo. Back up nothing here except the knowledge that a wrong webhook URL silently drops payment events.

Until this is done, [payments](./payments.md), [memberships](./memberships.md) and [session packs](./session-packs.md) run in **simulated mode** locally and on preview deploys. Production pay links say online payment is not set up. Staff can still record cash and checks.

## Activate the account

Create or sign in at the Stripe dashboard and finish activating it: business details, bank account for payouts, identity. Stripe asks for these. Nobody else should fill them in.

## Keys

In Vercel (Production only; leave Stripe unset on Preview):

- `STRIPE_SECRET_KEY` — `sk_test_…` / `sk_live_…`, or a restricted key `rk_…`
- `STRIPE_WEBHOOK_SECRET` — `whsec_…` from the webhook endpoint
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — `pk_…` (optional for Checkout today; needed later for cards on file)

A restricted key needs **Write** on Checkout Sessions, Refunds, Customers, Products, Prices, Subscriptions, and Billing Portal Sessions/Configurations, and **Read** on PaymentIntents, PaymentMethods and Invoices. If a test payment fails with a Stripe permission error, add the permission the log names.

## Webhook

Developers → Webhooks → Add destination. Endpoint:

`https://YOUR-DOMAIN/api/stripe/webhook`

Subscribe to exactly these events (the CMS ignores anything else):

- `checkout.session.completed`
- `checkout.session.async_payment_succeeded`
- `checkout.session.async_payment_failed`
- `charge.refunded`
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `invoice.paid`
- `invoice.payment_failed`

Subscription events are used when memberships is on. They are harmless if it is off. Events are stored and applied **once**. Replays from the Stripe dashboard should not double-pay or double-activate.

## Billing portal

In Stripe, enable the Customer billing portal. The CMS opens a portal session for the signed-in customer’s own Stripe customer:

- Payment method updates and invoice history stay on
- Cancellation follows **Let members cancel online** on admin → Memberships (on by default). When that is off, the portal configuration has cancellation disabled.

Use **test** keys while you try this. The no-cancel configuration is created in Stripe the first time it is needed, then cached.

## Payment methods

Cards and wallets are on by default. Turn on ACH Direct Debit in Stripe if you want US bank payments (cheaper for large invoices; they take a few business days). Checkout offers whatever you enabled.

## Test then live

1. Test-mode keys and a test-mode webhook (its own `whsec_…`).
2. Buy a [membership](./memberships.md) plan, a [session pack](./session-packs.md) if that switch is on, and pay a test [invoice](./invoices.md) (card `4242 4242 4242 4242`).
3. Confirm status in admin after the webhook, not only in the Stripe dashboard.
4. Switch to live keys and a live webhook before launch.
5. Make a small real payment, confirm it in admin → Payments, then refund it there.

Payouts (daily by default, instant payouts to a debit card) are in the Stripe dashboard, not in the CMS.
