> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Events

> The full event catalog, the delivery envelope, and how to verify a signature.

Subscribe to events instead of polling. **Webhook endpoints are managed in the dashboard** (Developers → Webhooks) — there is no public API for creating them. Add a URL, choose your events, copy the signing secret, and Duro delivers signed, retried `POST`s for the whole subscription lifecycle to your server. The mechanics are in the [webhooks chapter](/webhooks/delivery); this is the reference for the payloads you'll receive.

## The delivery envelope

Every delivery is a `POST` with this body:

```json theme={null}
{
  "id": "evt_01HKBZ4PQ9Z3K7T8V9X2Y1B5C6",
  "type": "subscription_payment_success",
  "createdAt": "2026-06-28T09:00:00.000Z",
  "data": {
    "subscriptionId": "sub_•••",
    "invoiceId": "inv_•••",
    "amount": 500000
  }
}
```

And these headers:

| Header            | Value                           |
| ----------------- | ------------------------------- |
| `Duro-Signature`  | `t=<unix>,v1=<hex hmac-sha256>` |
| `Duro-Event-Id`   | the event id (dedupe on this)   |
| `Duro-Event-Type` | the event type                  |

## The catalog

Event names are stable and canonical. Subscribe to exactly the ones you list when creating the endpoint (an empty list means *all*).

<AccordionGroup>
  <Accordion title="Subscription lifecycle" icon="repeat">
    | Event                                          | When                                                     |
    | ---------------------------------------------- | -------------------------------------------------------- |
    | `subscription_created`                         | A subscription is created (incl. via checkout).          |
    | `subscription_activated`                       | Trial converted, or first charge succeeded.              |
    | `subscription_updated`                         | A mutable field changed (e.g. cancel-at-period-end set). |
    | `subscription_plan_changed`                    | Plan changed (immediate or scheduled).                   |
    | `subscription_paused` / `subscription_resumed` | Paused or resumed.                                       |
    | `subscription_past_due`                        | A renewal failed; dunning has begun.                     |
    | `subscription_recovered`                       | Dunning won; the subscription is active again.           |
    | `subscription_unpaid`                          | Dunning exhausted; written off.                          |
    | `subscription_expired`                         | Reached `maxCycles`.                                     |
    | `subscription_cancelled`                       | Cancelled (immediately or at period end).                |
  </Accordion>

  <Accordion title="Payments" icon="money-bill">
    | Event                                  | When                                      |
    | -------------------------------------- | ----------------------------------------- |
    | `subscription_payment_success`         | A charge succeeded (renewal or checkout). |
    | `subscription_payment_failed`          | A charge failed.                          |
    | `subscription_payment_recovered`       | A previously-failed charge was recovered. |
    | `subscription_payment_refunded`        | An invoice was refunded.                  |
    | `subscription_payment_action_required` | The customer must update their card.      |
    | `invoice_created`                      | A renewal invoice was generated.          |
  </Accordion>
</AccordionGroup>

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

```javascript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

<Warning>
  Always verify against the **raw** body bytes. If your framework parses JSON before you can read the raw body, configure it to expose the raw payload (Duro does this internally with a raw-body saver for exactly this reason).
</Warning>

## Retries & replay

A non-2xx response is retried on an exponential backoff (`1m, 5m, 30m, 2h, 6h, 24h`, six attempts). 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.
