Skip to Content
LaunchfastDocs

Webhooks

How Dodo tells your app about payments, which events the kit handles, and the rules that keep billing correct.

When something happens in Dodo (a payment succeeds, a subscription renews or is cancelled, a refund goes through), Dodo sends a signed POST request to:

https://yourdomain.com/api/webhooks/dodo

The handler is in src/app/api/webhooks/dodo/route.ts, and the logic in src/lib/billing/dodo.ts.

Registering it

  • With setup: registered automatically when you deploy, and the secret is saved to Vercel as DODO_PAYMENTS_WEBHOOK_SECRET.
  • By hand: in Dodo, Developer → Webhooks → Add endpoint, enter the URL, select all events (or at least the ones below), click Create endpoint, and copy the signing secret from the endpoint's Overview tab into DODO_PAYMENTS_WEBHOOK_SECRET.

Test mode and live mode have separate webhook endpoints and secrets. When you switch to live, register the endpoint again in live mode.

Events handled

EventWhat the app does
subscription.* (active, renewed, on_hold, cancelled, expired, plan_changed…)Creates or updates the user's row in subscription: status, product, next billing date, cancel-at-period-end, price
payment.succeededAdds a row to payment, the ledger of every successful charge. A one-time payment grants access
refund.succeeded (full refunds only)Marks the payment as refunded, which removes access from a one-time purchase

Other events are accepted and ignored. Zero-amount charges that only update a card aren't recorded as revenue.

After each event that changes a user's billing, repository access is brought in line (invite after payment, removal after a full refund), if you use it.

Responses

StatusMeaningDodo's reaction
200 {"received":true}Processed (or ignored)Done
401 {"error":"Invalid signature"}The signature didn't match DODO_PAYMENTS_WEBHOOK_SECRETRetries later
500Something failed while saving, such as the database being unreachableRetries later

The rules

These are why billing stays correct even when things go wrong:

  • Every request is verified. The signature is checked with the request's headers and your secret. The Dodo SDK silently skips verification when headers aren't passed, so verifyWebhook() always passes them.
  • Safe to receive twice. Dodo retries, so the same event can arrive more than once. Payments are inserted once by their ID, and subscriptions are updated in place.
  • Old events can't overwrite new ones. Events can arrive out of order. A subscription event older than the stored one (by its timestamp) is ignored.
  • Users are linked through billing_customer, which is created on your server at checkout. Metadata from the browser is never trusted.

Seeing what was delivered

In Dodo, open Developer → Webhooks, select the endpoint, and check its delivery logs. Each attempt shows the status code your app returned, and you can resend an event from there.

On Vercel, check Logs for lines starting with [dodo]. For example, [dodo] payment.succeeded: unknown customer cus_… means the payment's customer isn't linked to any user in this database. See Unknown customer.

On this page