For AI agents: fetch the complete documentation index at /llms.txt, the full documentation corpus at /llms-full.txt, the agent skill at /skill.md, or this page as Markdown at /docs/web/stripe-payments.md (or send Accept: text/markdown).
Add Stripe payments
Create a free Stripe account, a product with a recurring price, a secret key, and the signed webhook that grants entitlements after checkout.
Beginner walkthrough
Prerequisites: the app running locally (Ship in 5 minutes) and a migrated database for real accounts (Set up database). You need an email address for the Stripe account — no card and no charges. Everything in this guide happens in test mode: fake money, test card numbers, unlimited retries before going live.
Quick terms: test mode is Stripe's sandbox — the dashboard's Test mode toggle keeps every key, product, and payment fake until you switch it off. A secret key (sk_test_…) is your server's password to Stripe. A product with a price is what customers buy; a price id (price_…) names it in code. A webhook is Stripe calling your server when a payment succeeds or a subscription changes, and the signing secret (whsec_…) proves the call really comes from Stripe.
What you create: a Stripe account, one product with a recurring price, a secret API key, and a webhook endpoint — about twenty minutes. Stripe hosts the checkout page itself; the kit creates the session server-side and redirects, so no publishable browser key is needed.
Keep scope in mind: this guide sells web subscriptions only. Purchases inside the iOS/Android app must go through Apple and Google billing — never Stripe Checkout — which is why mobile uses RevenueCat instead.
Create the Stripe account
1. Open stripe.com → Start now and sign up with your email.
2. The onboarding asks about your business — for test mode you can skip every question and still use the full sandbox; complete activation only before taking real payments.
Expected: you land on the dashboard with the Test mode toggle visible in the header. Keep it on for everything below — live keys belong in your production secret store later, never in a laptop env file.
Copy the API keys
1. In the dashboard open Developers → API keys.
2. You see two test keys: a Publishable key pk_test_… and a masked Secret key. Click Reveal test key next to the secret and copy the sk_test_… value.
The kit only needs the secret key: checkout sessions are created on the server and the user is redirected to Stripe's hosted page, so no publishable key lands in env vars. If a product you build later adds Stripe.js you would use the publishable key client-side; the kit's billing flow does not.
The sk_test_… key is a full credential for the sandbox account: it lives in apps/web/.env.local or a secret store, never in NEXT_PUBLIC_/EXPO_PUBLIC_ variables and never in Git.
Create the product and price
1. Open Product catalog → Add product.
2. Name it after your plan — Pro matches the kit's shipped plan — give it a price, and choose Recurring (monthly, for example): the kit's checkout creates subscription-mode sessions, which require a recurring price, not a one-time one.
3. Save, open the product, and copy the price id — it starts price_… (not prod_…, which is the product id).
The shipped pro plan already shows a Monthly/Yearly toggle: monthly checks out with STRIPE_PRICE_ID, and the yearly option needs a second recurring price in STRIPE_PRICE_ID_PRO_YEARLY — without it, a yearly checkout fails price-unconfigured. Change plan names, amounts, and what a purchase grants in apps/web/src/product-plans.ts — Stripe stores the money side, the plan file stores the entitlement side.
Add the webhook endpoint
The webhook is what actually grants access: the kit flips the entitlement only after a signed Stripe event arrives, so a checkout redirect alone proves nothing.
1. Open Developers → Webhooks → Add endpoint.
2. Endpoint URL: your public URL plus /api/billing/webhook — for example https://yourdomain.com/api/billing/webhook. The other billing providers get their own path under that directory; Stripe's receiver lives at the root.
3. Under Select events choose the events the kit handles: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, invoice.paid, invoice.payment_failed, customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted.
4. Create the endpoint, open its page, click Reveal under Signing secret, and copy the whsec_… value — each endpoint has its own secret.
Local development cannot receive Stripe's servers, so use the Stripe CLI instead: stripe login once, then stripe listen --forward-to localhost:3000/api/billing/webhook in a second terminal. It prints a temporary whsec_… — that value, not the dashboard one, goes in .env.local while stripe listen runs.
Add the values to the app
Set the lines below in apps/web/.env.local. BILLING_PROVIDER=stripe selects the adapter; the other three are the key, the price, and the signing secret. Enabling the provider with any of them missing fails startup and names the variable.
All three STRIPE_* values are server-only — never NEXT_PUBLIC_/EXPO_PUBLIC_, never committed; production gets them through the host's secret manager (the deploy guide covers Secret Manager). Restart the dev server after editing.
# apps/web/.env.local
BILLING_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_test_…
STRIPE_PRICE_ID=price_…
STRIPE_WEBHOOK_SECRET=whsec_… # `stripe listen` value locally; endpoint signing secret in production
# STRIPE_PRICE_ID_PRO_YEARLY=price_… # required for the shipped Yearly toggleVerify and troubleshoot
Start stripe listen --forward-to localhost:3000/api/billing/webhook and leave it running, restart the dev server, then open /billing, choose the Pro plan, and finish Stripe's hosted checkout with the test card 4242 4242 4242 4242, any future expiry, and any CVC.
Expected: the app returns to /billing?checkout=success and the workspace's entitlement flips active — the listen terminal logs the delivered events, and Developers → Webhooks → your endpoint on the dashboard shows green deliveries.
To exercise the receiver without clicking through checkout, stripe trigger checkout.session.completed sends a synthetic event through the listen forwarder.
Common failures: a 400 on every delivery means the signing secret does not match that endpoint — recopy whsec_… for the right endpoint and update env; silent no-ops mean BILLING_PROVIDER is still local; and sk_live_ keys mixed into a test setup mean you copied the wrong-mode keys — keep test and live strictly separate.
The kit ships the same one-contract adapter for Lemon Squeezy, Creem, Polar, Whop, Paddle, Dodo, and PayPal — same flow, different console and env names — and mobile purchases stay on RevenueCat, not Stripe.
If you want to continue setup, read Add Resend email.