Agent skill
Dodo Payments Webhooks Skill
Receive and verify Dodo Payments webhooks, which follow the Standard Webhooks specification. Use when setting up a Dodo Payments webhook endpoint, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling events like payment.succeeded, payment.failed, refund.succeeded, dispute.opened, subscription.active and license_key.created.
Install this skill
npx skills add hookdeck/webhook-skills --skill dodo-payments-webhooks
Dodo Payments is a merchant-of-record payments platform covering payments, refunds, disputes, subscriptions, license keys, entitlements, credits, dunning and payouts. It POSTs a signed JSON event to each endpoint you add under Developer > Webhooks. Signatures follow Standard Webhooks, so each delivery carries webhook-id, webhook-timestamp and webhook-signature headers.
When to Use This Skill
- How do I receive Dodo Payments webhooks?
- How do I verify a Dodo Payments webhook signature (
webhook-signature)? - Where do I find my Dodo Payments webhook secret (
whsec_...)? - How do I handle
payment.succeeded,refund.succeeded,dispute.openedorsubscription.activeevents? - Why is my Dodo Payments webhook verification failing, or failing only on retries and replays?
- Why does
dodo wh triggerfail signature verification? (Mock events are unsigned.)
Verification (core)
| Header | Value |
|---|---|
webhook-id | Identifies the message; retries reuse it. Your idempotency key (the body has no event id) |
webhook-timestamp | Unix seconds |
webhook-signature | One or more space-separated v1,<base64> entries |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256, base64 output. The key is the base64-decoded part of the whsec_... secret after the prefix. Compare against every v1, entry; ignore other versions. If webhook-id is absent, read svix-id, svix-timestamp and svix-signature instead, as Hookdeck does. The examples use the standardwebhooks library (npm and PyPI):
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyDodo(rawBody, headers, secret = process.env.DODO_PAYMENTS_WEBHOOK_SECRET) {
const p = headers['webhook-id'] ? 'webhook' : 'svix'; // fall back to the svix-* names
new Webhook(secret).verify(rawBody, {
'webhook-id': headers[`${p}-id`],
'webhook-timestamp': headers[`${p}-timestamp`],
'webhook-signature': headers[`${p}-signature`],
}, { jsonParse: false }); // throws on a bad signature or stale timestamp → respond 401
}
The library always enforces a fixed 5-minute timestamp window. Hookdeck retries and replays resend the original webhook-timestamp, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in references/verification.md, which can drop or widen the window. In Express, read the body with express.raw({ type: '*/*' }) so any content type verifies, and parse JSON only after the check.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Type and Payload
Every body is the same envelope, and the event name is the top-level type:
{
"business_id": "bus_H4ekzPSlcg",
"type": "payment.succeeded",
"timestamp": "2024-01-15T10:30:00Z",
"data": { "payload_type": "Payment" }
}
timestamp is when the event occurred (ISO 8601). data.payload_type names the resource kind (Payment, Subscription, Refund, Dispute, LicenseKey, CreditLedgerEntry, CreditBalanceLow, AbandonedCheckout, DunningAttempt, EntitlementGrant or Payout), not the event. The other data fields vary per event.
Common Event Types
| Event | Fired when |
|---|---|
payment.succeeded | A payment completes |
payment.failed | A payment attempt fails (declined card, insufficient funds, other errors) |
payment.processing | A payment is being processed |
payment.cancelled | A payment is cancelled |
refund.succeeded / refund.failed | A refund completes or fails |
dispute.opened | A cardholder opens a dispute on a payment |
dispute.won / dispute.lost | The dispute is resolved in your favour / the cardholder's favour |
subscription.active | A subscription becomes active |
subscription.renewed | A subscription renews for the next billing period |
subscription.on_hold | A subscription is temporarily put on hold due to a failed renewal |
subscription.cancelled | The merchant or customer cancels a subscription |
license_key.created | A new license key is created for a product |
These are the events the examples route. Dodo documents 48 in all (the list may not be exhaustive): more payment, dispute (challenged, accepted, cancelled, expired) and subscription (updated, past_due, paused, unpaused, plan_changed, update_payment_method, failed, expired) events, plus credit.*, abandoned_checkout.*, dunning.*, entitlement_grant.* and payout.*. Ignore types you don't recognise. See references/overview.md.
Delivery Semantics
Return a 2xx within the 30-second connect and read timeout, then process asynchronously. Anything else is retried with exponential backoff, 8 attempts in all (immediately, then after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h). Deduplicate on webhook-id. Events can arrive out of order: use timestamp if order matters, and note that payout events are neither terminal nor ordered. Return 400 for missing headers and 401 for a bad signature. Dodo gives no fixed source IP list: verify the signature instead of allowlisting.
Environment Variables
DODO_PAYMENTS_WEBHOOK_SECRET=whsec_xxxxx # full value from the endpoint's Overview tab; handlers fail closed (500) without it
Dodo's own SDK docs call this DODO_. Rotating the secret keeps the old one valid for 24 hours.
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 dodo-payments --path /webhooks/dodo-payments
Set the printed URL as the endpoint URL under Developer > Webhooks, then use the endpoint's Testing tab to send a signed example. dodo wh trigger mock events are not signed.
Reference Materials
- references/overview.md - Event catalog, envelope and resource kinds
- references/setup.md - Add an endpoint, find and rotate the secret, test
- references/verification.md - Library and manual verification, gotchas, debugging