Payment Issues
Troubleshoot checkout, webhooks, subscriptions, and other payment problems.
Webhook not receiving events
Symptom: Payments complete but your database doesn't update. No webhook events appear in server logs.
Cause: Webhook URL not configured, not publicly accessible, or signature verification failing.
Fix:
-
Check the webhook URL in your DodoPayments dashboard. It should be:
- Production:
https://yourdomain.com/api/webhook/dodo - Never
localhost— DodoPayments can't reach your local machine
- Production:
-
For local development, use a tunnel:
# Install ngrok
npm install -g ngrok
# Start a tunnel to your dev server
ngrok http 3000
Copy the ngrok URL (e.g., https://abc123.ngrok.io) and set it as the webhook URL in DodoPayments: https://abc123.ngrok.io/api/webhook/dodo
-
Check webhook logs in the DodoPayments dashboard. You'll see if events were sent and what response your server returned.
-
Verify the route exists and returns 200:
curl -X POST https://yourdomain.com/api/webhook/dodo \
-H "Content-Type: application/json" \
-d '{"type": "test"}'
You should get a response (even if it's an error about invalid signature — that means the route is reachable).
Checkout session creation fails
Symptom: Clicking "Upgrade" or "Subscribe" shows an error. The checkout page never loads.
Cause: Invalid API key, missing price ID, or misconfigured products.
Fix:
- Check your API key:
DODO_API_KEY="your-api-key"
Make sure it's the correct key for your environment (test vs. live).
- Check price IDs exist in your DodoPayments dashboard:
DODO_STARTER_PRICE_ID="price_starter_monthly"
DODO_PRO_PRICE_ID="price_pro_monthly"
These must match actual product/price IDs in your DodoPayments account.
- Check server logs for the specific error:
# Vercel
vercel logs --follow
# Local
# Check terminal output
- Test with curl:
curl -X POST http://localhost:3000/api/checkout \
-H "Content-Type: application/json" \
-H "Cookie: better-auth.session_token=..." \
-d '{"priceId": "price_starter_monthly"}'
Subscription status not updating
Symptom: Payment goes through but the user's subscription status stays unchanged in the database.
Cause: Webhook handler not processing events correctly.
Fix:
-
Check webhook delivery: In the DodoPayments dashboard, go to Webhooks and check recent deliveries. Look for failed attempts (non-200 responses).
-
Check the webhook handler at
src/lib/payments/webhook.ts:- Is the event type being handled? Check the
switchstatement coverspayment.completed,subscription.updated, andsubscription.cancelled. - Is the database query correct? Log the event data to verify it matches your schema.
- Is the event type being handled? Check the
-
Check signature verification:
// Make sure the secret matches
const isValid = verifyWebhook(body, signature);
console.log("Webhook signature valid:", isValid);
console.log("Event type:", JSON.parse(body).type);
- Manually test: Trigger a test webhook from DodoPayments dashboard or resend a failed event.
Test mode vs. live mode confusion
Symptom: Payments work in development but fail in production, or vice versa.
Cause: Using test API keys in production or live keys in development.
Fix:
| Environment | Key type |
|---|---|
| Development (localhost) | Test keys |
| Staging/Preview | Test keys |
| Production | Live keys |
In DodoPayments, test and live keys are separate. Make sure Vercel has the live key and your .env.local has the test key.
Test cards only work with test keys. Real cards only work with live keys. Never mix them.
"Currency not supported" error
Symptom: Checkout fails with a currency-related error.
Cause: Your DodoPayments account or product isn't configured for the customer's currency.
Fix:
- Check your product settings in DodoPayments — ensure the correct currencies are enabled
- DodoPayments as a Merchant of Record handles currency conversion automatically, but your products need to have prices set
- If selling globally, let DodoPayments handle local pricing through their dashboard
Double charges or duplicate subscriptions
Symptom: A user has multiple subscription records or was charged twice.
Cause: Webhook received multiple times (retries on timeout), or checkout completed without deduplication.
Fix:
- Add idempotency to your webhook handler:
async function handlePaymentCompleted(data: PaymentCompletedData) {
// Check if subscription already exists
const existing = await db
.select()
.from(subscriptions)
.where(eq(subscriptions.dodoSubscriptionId, data.subscriptionId))
.limit(1);
if (existing.length > 0) {
// Already processed — skip
return;
}
// Create subscription
await db.insert(subscriptions).values({ ... });
}
- Return 200 quickly: If your webhook handler times out, DodoPayments retries. Make sure you return
{ received: true }before doing heavy processing, or process asynchronously.
Subscription cancellation not working
Symptom: User cancels but their access isn't revoked, or the status doesn't update.
Cause: Cancellation webhook not being handled, or subscription check not using the right field.
Fix:
- Make sure
subscription.cancelledis handled in your webhook:
case "subscription.cancelled":
await db
.update(subscriptions)
.set({ status: "cancelled", updatedAt: new Date() })
.where(eq(subscriptions.dodoSubscriptionId, event.data.subscriptionId));
break;
- Check your subscription status query — it should verify
status === "active":
function hasActiveSubscription(subscription: Subscription | null): boolean {
return subscription?.status === "active";
}
Pricing page shows wrong amounts
Symptom: Prices displayed on the landing page don't match DodoPayments checkout.
Cause: Plan definitions in code are out of sync with DodoPayments products.
Fix:
Update src/lib/payments/plans.ts to match your DodoPayments product pricing. The prices in this file are for display only — the actual charge is determined by the DodoPayments price ID.
export const PLANS = {
starter: {
name: "Starter",
priceId: process.env.DODO_STARTER_PRICE_ID!,
price: 29, // Must match DodoPayments
},
};
Still stuck?
- Check DodoPayments documentation for API-specific issues
- Look at webhook delivery logs in the DodoPayments dashboard
- Ask in the Telegram community for help