> For the complete documentation index, see [llms.txt](https://developers.share.inc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.share.inc/webhooks/envelope.md).

# Envelope

Every webhook delivery POSTs a single JSON object that follows the same top-level shape regardless of which event you subscribed to. Parse the envelope first, decide what to do based on `type`, then read the event-specific payload from `data.current`.

## Top-level shape

```json
{
  "id": "evt_2c8f2a1e9b4f4d8aa39e7b1c5d2f0a91",
  "type": "subscription.plan_change_applied",
  "apiVersion": "2026-05-22",
  "occurredAt": "2026-05-26T09:14:22.000Z",
  "providerId": "prv_01HABC...",
  "correlationId": "84d6b7e2-1c4a-4f3b-9a32-7b8c0d1e2f30",
  "livemode": true,
  "data": {
    "object": "subscription",
    "current": {
      "id": "sbn_01H...",
      "planId": "pln_NEW",
      "status": "ACTIVE"
    },
    "previous": {
      "nextPlanId": "pln_NEW"
    },
    "changedFields": ["planId"]
  }
}
```

| Field                | Type                          | Notes                                                                                                                                            |
| -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                 | `evt_<32 hex>`                | Idempotency key — stable across retries. Persist it and ignore duplicates                                                                        |
| `type`               | `<resource>.<action>`         | The event name. The full catalog is at [Event catalog](/webhooks/event-catalog.md)                                                               |
| `apiVersion`         | date string                   | Pinned per subscription. New versions plug in alongside; existing subscriptions keep receiving the pinned shape                                  |
| `occurredAt`         | ISO-8601 UTC                  | When the source action actually happened, not when Share delivered it                                                                            |
| `providerId`         | string                        | Your provider ID. Use it as a multi-tenant guard in your receiver                                                                                |
| `correlationId`      | UUID                          | Joins to Share's `audit_logs` for support tracing. Include it in any ticket you open                                                             |
| `livemode`           | boolean                       | `true` in production, `false` in sandbox. Always branch on this before moving real money                                                         |
| `data.object`        | string                        | The resource type carried in `data.current` — `subscriber`, `subscription`, `payment`, `billing_cycle`, `network`, `plan`, `charge`, or `refund` |
| `data.current`       | object                        | Full snapshot of the resource at event time. You should not need to call back to the REST API                                                    |
| `data.previous`      | object \| absent              | Changed fields only — absent on `*.created`, `*.deleted`, and a handful of ephemeral events (see below)                                          |
| `data.changedFields` | string\[] \| absent           | Field names that changed, when the event semantics expose them                                                                                   |
| `data.rejection`     | `{ reason, note? }` \| absent | Present on `*_rejected` events (plan-change, override)                                                                                           |

`apiVersion` is **pinned per subscription**, not per request. When Share ships a new envelope version, existing subscriptions keep receiving the version they registered against. To opt in, send `PUT /v1/webhooks/:id` with a new `apiVersion` value once you have updated your receiver.

## Headers

Every signed delivery carries these headers (see [Signature verification](/webhooks/verification.md) for the contract):

| Header                     | Meaning                                                              |
| -------------------------- | -------------------------------------------------------------------- |
| `X-Share-Signature`        | `sha256=<hex(HMAC-SHA256(signingSecret, "<timestamp>.<raw-body>"))>` |
| `X-Share-Timestamp`        | Unix seconds at signing — included in the signed string              |
| `X-Share-Event-Type`       | Matches `type` in the body                                           |
| `X-Share-Event-Id`         | Matches `id` in the body; stable across retries (idempotency key)    |
| `X-Share-Delivery-Id`      | Unique per attempt (changes on retries)                              |
| `X-Share-Delivery-Attempt` | 1-based attempt counter                                              |

Custom headers you registered via `headers[]` ride alongside these. Share's own `X-Share-*` headers always win on collision — a partner-supplied `X-Share-Signature` is silently dropped.

## When `previous` is omitted

`previous` is absent — not `null` — on events whose semantics describe a new fact rather than a state transition:

| Event family                                              | Why `previous` is absent                                       |
| --------------------------------------------------------- | -------------------------------------------------------------- |
| `*.created` (e.g. `subscriber.created`)                   | There was no prior state                                       |
| `*.deleted` (e.g. `subscriber.deleted`)                   | Nothing meaningful to compare against                          |
| `billing.cycle_ending`                                    | A scheduled signal, not a transition                           |
| `network.session_started` / `session_ended` / `throttled` | Ephemeral session signals                                      |
| `refund.issued`                                           | A new resource, not a state change                             |
| `subscription.plan_change_rejected`                       | The rejection is the fact; `data.rejection` carries the reason |
| `subscription.override_queued` / `override_rejected`      | Same — no prior override existed                               |

Treat the **presence** of `previous` as the signal of a transition. Do not check `previous` for equality with `null`.

## Idempotency and retries

Share retries non-2xx deliveries with exponential backoff. Retries reuse the same `id` and the same `X-Share-Event-Id` — every retry of a given event is byte-identical at the body level, only the headers move (`X-Share-Delivery-Id`, `X-Share-Delivery-Attempt`, `X-Share-Timestamp`, `X-Share-Signature` all change).

The contract on your side:

1. Persist `id` (or `X-Share-Event-Id` — same value) before doing the side-effect.
2. On a second delivery of the same `id`, short-circuit with 2xx and skip the work.
3. Do not deduplicate on `data.current` field values — two genuinely distinct events can carry the same snapshot if the underlying resource changed and then changed back.

See [Setup → Step 4](/webhooks/setup.md#step-4-handle-retries-idempotently) for the full retry posture.

## Next steps

* [Event catalog](/webhooks/event-catalog.md) — every event you can subscribe to and what each one carries in `data.current`
* [Signature verification](/webhooks/verification.md) — the HMAC contract in detail
* [Setup](/webhooks/setup.md) — register a subscription
