Checkout and billing
What happens from the moment someone clicks a pricing button until their plan is unlocked, and how customers manage their billing.
The checkout flow
The visitor clicks a plan
The pricing button posts the plan's slug to the startCheckout server action (src/lib/billing/actions.ts).
Signed out? Sign up first
Visitors who aren't signed in go to /sign-up?plan=<slug>. The chosen plan is kept through sign-up and email confirmation, and the dashboard shows Finish getting [plan] with a Continue to checkout button.
A Dodo customer is found or created
The first checkout creates a Dodo customer for the user and stores the link in the billing_customer table. This is how payments are tied to users. User IDs sent from the browser are never trusted.
Dodo's hosted checkout
The user is sent to Dodo's checkout page to pay. Dodo handles the card form, UPI, tax and receipts.
Back to the dashboard
After paying, Dodo sends the user to /dashboard?checkout=success. The page shows "Payment received. Unlocking your plan, this takes a few seconds…" and checks again every 2.5 seconds.
The webhook unlocks the plan
Meanwhile Dodo sends a webhook to /api/webhooks/dodo. The app records the payment or subscription, and on its next check the dashboard shows the plan. See Webhooks.
If nothing arrives within 60 seconds, the page says: "Your payment went through, but confirming it is taking longer than usual. Please don't pay again." See Plan doesn't unlock after paying.
Who has access
The rule lives in one place, getBillingStatus() in src/lib/billing/index.ts:
- A paid one-time purchase gives access for good (until it's fully refunded).
- Otherwise, a subscription with status
activegives access. - Anything else is the free tier.
| Subscription status | Access | Dashboard says |
|---|---|---|
active | Yes | "Renews on [date]." or "Cancels on [date]. You keep access until then." |
on_hold (a renewal payment failed) | No | "Your last payment failed. Update your payment method to keep access." |
cancelled, expired, failed | No | "Upgrade to unlock everything." |
The rule only reads your own database, never Dodo's API, so page loads stay fast. Webhooks keep the tables up to date.
The billing portal
The Manage billing button on the dashboard opens Dodo's customer portal, where customers can update their card, see invoices and cancel subscriptions. Users who never started a checkout are sent to the pricing section instead.
Checkout consent (digital goods)
Selling instantly delivered digital goods (downloads, source code) with no refunds to EU or UK buyers? They must agree to give up their 14-day withdrawal right before paying. Set the wording in src/config.ts:
checkoutConsent: 'I want immediate access and understand I lose my right to withdraw once the download starts.',A required checkbox then appears before checkout, is checked again on the server, and the time it was accepted is stored on the Dodo order (checkout_consent_at). Leave it null for a normal SaaS. Have the wording reviewed for the countries you sell in.
Upgrading from a subscription to a one-time plan
If a user buys a one-time plan while a subscription is still active, the dashboard warns them that the subscription still renews and asks them to cancel it in Manage billing, so they aren't charged again.
Refunds
Issue refunds in the Dodo dashboard. A full refund marks the payment as refunded, which removes access that came from that one-time purchase (and repository access, if you use it). Partial refunds don't change access.