Skip to content
All guides

Payments

The payment provider contract, with Dodo Payments, Lemon Squeezy or Stripe behind it: hosted checkout, the customer portal, signed webhooks, the subscription lifecycle and entitlements.

Updated 2026-10-23

Choosing a provider

Every payment vendor implements the same contract, so the checkout, the portal and the webhook route do not change when the provider changes. The deployment names its provider with PAYMENTS_PROVIDER, and the default is dodo so an existing deployment keeps working.

A provider that is named but not configured fails with an actionable error. It never falls back to another vendor, because a checkout taken by the wrong provider is worse than a checkout that refuses to start.

  • dodo: a merchant of record that settles the taxes and carries the chargeback risk.
  • lemonsqueezy: an alternative merchant of record with the same contract.
  • stripe: a card processor. The merchant keeps the tax and the chargeback responsibility.
.env.localbash
PAYMENTS_PROVIDER=dodo
# PAYMENTS_PROVIDER=lemonsqueezy
# PAYMENTS_PROVIDER=stripe

How a subscription is created

The checkout endpoint creates a hosted checkout session with the selected provider for the signed in account. The account identifier is written into the session metadata, so the webhook that arrives later can bind the provider customer to the right user without trusting the browser.

Access is never granted from the browser redirect. The subscription state is written only when a signed webhook is processed.

  1. Request checkoutPOST /api/billing/checkout-session with the target plan id.
  2. RedirectThe browser follows the checkout URL returned by the selected provider.
  3. WebhookThe provider signs the event and posts it to /api/webhooks/[provider].
  4. SyncThe subscription record and the entitlements are updated.

Webhook verification

The route resolves the provider, then calls that provider's verifier. A signature scheme is never shared across vendors: each implementation verifies its own header and its own secret.

Dodo Payments follows the Standard Webhooks specification. The signature is an HMAC SHA-256 over the webhook id, the timestamp and the raw body, keyed with the base64 decoded webhook secret. A payload more than five minutes away from the server clock is rejected.

  • The raw body is read as text. It is never parsed before verification.
  • A missing, malformed or stale signature returns a 400 and changes nothing.
  • The webhook id is the idempotency key, so a redelivery is acknowledged and skipped.

Subscription lifecycle

These events change the entitlement state of an account.

  • subscription.active: binds a Dodo customer to an account. Dodo sends the dotted event names, and the customer arrives as a nested object.
  • subscription.updated: any field change, used to keep state in sync without polling.
  • subscription.renewed and payment.succeeded: a successful payment.
  • subscription.cancelled and subscription.expired: the terminal state.

Entitlements

Entitlements are derived from the plan and the subscription state, never from anything the browser sends. A capability check runs on the server before every protected billing action.