Payment errors
Checkout failures, plans that don't unlock, webhook problems and billing questions with Dodo Payments.
The plan doesn't unlock after paying
The dashboard says "Payment received. Unlocking your plan, this takes a few seconds…" and then, after a minute:
Your payment went through, but confirming it is taking longer than usual. Please don't pay again.The payment worked in Dodo; your app just hasn't heard about it. Plans unlock when Dodo's webhook reaches your app. Work through these:
On your computer?
Dodo can't reach localhost. That's expected. Use a tunnel.
Is a webhook registered, in the right mode?
In Dodo, Developer → Webhooks, with the Live Mode switch matching your API key's mode. There should be an endpoint for https://<your-site>/api/webhooks/dodo. Test mode and live mode have separate endpoints. If it's missing, run npm run setup with deploy, or add it by hand.
What did your app answer?
Open the endpoint and look at the delivery log:
| Status | Go to |
|---|---|
401 | Webhook returns 401 |
404 | The URL is wrong: it must end in /api/webhooks/dodo, on your current address |
500 | Webhook returns 500 |
307 / 308 | The URL redirects (for example, to www). Use the final address |
200 but still locked | Webhook returns 200 but nothing unlocks |
| No attempts | The endpoint is for another mode, or is disabled |
Fix, then resend
After fixing, resend the failed events from the delivery log. Handlers are safe to run twice. The customer's plan unlocks on their next page load.
Webhook returns 401 {"error":"Invalid signature"}
Why: DODO_PAYMENTS_WEBHOOK_SECRET doesn't match the endpoint that sent the event. Usually it's the secret of a different endpoint (test vs live, old vs new URL), or it was never set.
Fix: open the endpoint in Dodo, copy the signing secret from its Overview tab, set it as DODO_PAYMENTS_WEBHOOK_SECRET (in Vercel for the live site, then redeploy; in .env.local locally, then restart npm run dev). Resend the events.
Webhook returns 500
Why: the app crashed while saving the event. Find the reason in Vercel's Logs (filter by /api/webhooks/dodo). Common causes:
| Log message | Fix |
|---|---|
Missing environment variable DODO_PAYMENTS_WEBHOOK_SECRET | Set it, redeploy |
Missing environment variable DATABASE_URL | Set it, redeploy |
| Database connection errors | Check the Neon project is running |
Dodo retries failed events automatically. You can also resend them once fixed.
Webhook returns 200 but nothing unlocks
[dodo] payment.succeeded: unknown customer cus_…
Why: the event's Dodo customer isn't linked to any user in this database. Payments are linked through the billing_customer table, created when the user starts checkout through your app. It happens when:
- The purchase was made from a Dodo payment link or the Dodo dashboard, not your site's pricing button.
- Production uses a different database from the one the checkout started on (for example, testing locally with a tunnel while a production webhook also receives the event).
Fix: always start purchases from your site's pricing buttons. For the second case, make sure the webhook endpoint points at the app whose database started the checkout.
Paid, but the dashboard shows "Free"
Why: the payment is recorded, but its product ID doesn't match any plan. Plans are matched through their DODO_PRODUCT_… variable, so:
- The variable is missing in Vercel (check Settings → Environment Variables), or
- It holds a product from the other mode (a test product ID in live mode, or the reverse), or
- The product was bought before you changed the variable.
Fix: set each plan's variable to the product ID in the mode you're using, and redeploy. One-time purchases of a product no longer in your config still show as paid.
Plan "…" has no Dodo product ID
Plan "pro" has no Dodo product ID. Run `npm run setup` or set DODO_PRODUCT_PRO in .env.local.Why: the plan's productEnv variable is empty.
Fix:
- Locally: run
npm run setup(keep the other steps). It creates the product and fills in the variable. Restartnpm run dev. - On Vercel: add the variable with the product ID, and redeploy.
- Just added the plan to
src/config.ts? See Add a plan.
Unknown plan "…".
Why: a checkout was started for a slug that isn't in plans. Usually a link like /sign-up?plan=old-name from before you renamed or removed a plan.
Fix: update links to use a current slug. Avoid renaming a plan's slug after launch.
Missing environment variable DODO_PAYMENTS_API_KEY
Why: checkout or the billing portal needs the API key, and it isn't set where the app runs.
Fix: set it in .env.local (restart npm run dev) or in Vercel (redeploy).
401 Unauthorized from Dodo at checkout
Why: the API key doesn't match DODO_PAYMENTS_ENVIRONMENT. A live key with test_mode, or the reverse, is rejected.
Fix: run npm run setup and redo Dodo Payments: it detects the key's mode and sets the variable for you. Or set DODO_PAYMENTS_ENVIRONMENT to live_mode or test_mode by hand to match the key.
403 INSUFFICIENT_PERMISSION at checkout
Why: the API key doesn't have Write access to everything the app uses (customers, checkouts, the billing portal).
Fix: in Dodo, Developer → API Keys → Edit Permissions, click Select all and choose Write.
404 Customer not found at checkout
Why: this used to happen after switching from test to live: the stored customer belonged to test mode.
Fix: current versions of the kit replace the customer automatically. Pull the latest kit.
Checkout shows a different price from my website
Why: your site shows amount from src/config.ts, but Dodo charges its product's price. Setup doesn't change a product's price once created.
Fix: see Changing a plan's price.
"Please accept the checkout terms to continue."
Why: checkoutConsent is set in src/config.ts, and the checkbox wasn't ticked. The server checks it even if the form is bypassed.
Fix: tick the box. If you don't sell no-refund digital goods, set checkoutConsent: null. See Checkout consent.
"Manage billing" takes me to the pricing section
Why: the user has never started a checkout, so there's no Dodo customer to open a portal for.
Fix: nothing to fix: buy a plan first.
"Your last payment failed. Update your payment method to keep access."
Why: a subscription renewal failed and Dodo put it on hold. Access is paused until it's paid.
Fix (for your customer): click Manage billing, update the card. Dodo retries the charge, the subscription becomes active, and access returns.
Customer was charged but cancelled, and still has access
That's expected. Cancelling stops the next renewal. The dashboard shows "Cancels on [date]. You keep access until then.", and access ends on that date.
"You own [plan], but your old subscription still renews"
Why: the user bought a one-time plan while a subscription was still active.
Fix (for your customer): cancel the subscription in Manage billing so it doesn't charge again.
Test purchases appear in /admin revenue
Why: test-mode payments are stored in the same tables. See Clear test data.
Checkout charged a real card on localhost
Why: .env.local holds a live API key. Setup writes the key you give it there, including when switching to live.
Fix: refund the payment in Dodo. Put test values back in .env.local; see Keep testing locally.