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.opened or subscription.active events?
  • Why is my Dodo Payments webhook verification failing, or failing only on retries and replays?
  • Why does dodo wh trigger fail signature verification? (Mock events are unsigned.)

Verification (core)

HeaderValue
webhook-idIdentifies the message; retries reuse it. Your idempotency key (the body has no event id)
webhook-timestampUnix seconds
webhook-signatureOne 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

EventFired when
payment.succeededA payment completes
payment.failedA payment attempt fails (declined card, insufficient funds, other errors)
payment.processingA payment is being processed
payment.cancelledA payment is cancelled
refund.succeeded / refund.failedA refund completes or fails
dispute.openedA cardholder opens a dispute on a payment
dispute.won / dispute.lostThe dispute is resolved in your favour / the cardholder's favour
subscription.activeA subscription becomes active
subscription.renewedA subscription renews for the next billing period
subscription.on_holdA subscription is temporarily put on hold due to a failed renewal
subscription.cancelledThe merchant or customer cancels a subscription
license_key.createdA 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_PAYMENTS_WEBHOOK_KEY. 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →