POSTs for the whole subscription lifecycle to your server. The transport mechanics are in the webhooks chapter; this page is the shared envelope reference, and the per-event payloads are split by category in the left sidebar.
The catalog is defined once as DuroEvent in @duro/billing (packages/billing/src/events.ts) and served live at GET /v1/events/types. Every payload is assembled by WebhookPayload.build (packages/merchant-api/src/webhooks/payload.ts); the samples on each page match that builder field for field.
Events by category
Subscription events
State-machine transitions: created, activated, updated, plan changed, paused/resumed, past due, recovered, unpaid, cancelled, expired, incomplete expired.
Payment & invoice events
Charge and invoice outcomes: invoice created, payment success, failed, recovered, refunded, and action required.
Checkout events
Hosted and inline checkout session outcomes: completed, failed, and cancelled.
Payout & bill events
Money-out and VAS outcomes: payout initiated, completed, failed, and bill payment completed.
The delivery envelope
Every delivery is aPOST with this body:
Related resources are expanded inline: whenever the raw event carries a
subscriptionId, invoiceId, customerId, or planId, the builder replaces it with the full subscription, invoice, customer, or plan object (whichever apply) and drops the flat *Id field. A top-level status is also dropped once the subscription object (which already carries status) is present. Flat fields the builder does not know how to expand (for example payoutId, checkoutToken, failureCode) are passed through unchanged.
The expanded objects are compact projections, always shaped like this:
Verifying a signature
Recompute the HMAC and compare in constant time. The signed payload is${t}.${rawBody}; use the raw request body, not a re-serialised object. Duro signs with WebhookSigner in @duro/crypto; the default replay tolerance is 300 seconds (5 minutes).
Retries & replay
A non-2xx response (or a network error) is retried on an exponential backoff:1m, 5m, 30m, 2h, 6h, 24h, six attempts, after which the delivery is marked failed. Make your handler idempotent: dedupe on duro-event-id, because a slow 200 can still be retried. You can also replay any delivery from the dashboard’s delivery inspector.
Retrieve a payload
Have the event id (from aduro-event-id header, the dashboard, or an earlier payload) but missed or couldn’t verify the delivery? Re-fetch the exact same body any time; there is no need to store payloads yourself:
data field is byte-for-byte identical to what Duro POSTed, the same enriched objects, which makes reconciliation a single call. Full reference: GET /v1/events/{id}/payload.
The response also flags provenance so you know whether it was actually sent:
- If the event has been delivered to at least one endpoint, you get that delivery’s stored body plus
"delivered": true. - If it has not (no endpoint was subscribed, or delivery is still pending), you get a freshly-built preview plus
"delivered": false, "preview": true(the exact bytes an endpoint would receive).
GET /v1/events/{id}/deliveries.