KitRocket

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:

  1. 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
  2. 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

  1. Check webhook logs in the DodoPayments dashboard. You'll see if events were sent and what response your server returned.

  2. 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:

  1. Check your API key:
DODO_API_KEY="your-api-key"

Make sure it's the correct key for your environment (test vs. live).

  1. 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.

  1. Check server logs for the specific error:
# Vercel
vercel logs --follow

# Local
# Check terminal output
  1. 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:

  1. Check webhook delivery: In the DodoPayments dashboard, go to Webhooks and check recent deliveries. Look for failed attempts (non-200 responses).

  2. Check the webhook handler at src/lib/payments/webhook.ts:

    • Is the event type being handled? Check the switch statement covers payment.completed, subscription.updated, and subscription.cancelled.
    • Is the database query correct? Log the event data to verify it matches your schema.
  3. 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);
  1. 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:

EnvironmentKey type
Development (localhost)Test keys
Staging/PreviewTest keys
ProductionLive 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:

  1. Check your product settings in DodoPayments — ensure the correct currencies are enabled
  2. DodoPayments as a Merchant of Record handles currency conversion automatically, but your products need to have prices set
  3. 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:

  1. 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({ ... });
}
  1. 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:

  1. Make sure subscription.cancelled is 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;
  1. 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?

On this page