Skip to Content
LaunchfastDocs

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:

StatusGo to
401Webhook returns 401
404The URL is wrong: it must end in /api/webhooks/dodo, on your current address
500Webhook returns 500
307 / 308The URL redirects (for example, to www). Use the final address
200 but still lockedWebhook returns 200 but nothing unlocks
No attemptsThe 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 messageFix
Missing environment variable DODO_PAYMENTS_WEBHOOK_SECRETSet it, redeploy
Missing environment variable DATABASE_URLSet it, redeploy
Database connection errorsCheck 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.

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. Restart npm 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.

On this page