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

# Payload structure

> What every Boble webhook delivery looks like — the envelope, the headers, and the data object.

Every event Boble sends to your endpoint follows the same structure. The fields that change between events are the `type` and what sits inside `data.object`. Everything else is constant.

## The envelope

```json theme={null}
{
  "id": "0d6d31a3-2b9e-4d9f-9a51-3b9c3f8e6c10",
  "type": "user_promotion.access_granted",
  "api_version": "2026-01-01",
  "created": 1717497600,
  "livemode": true,
  "data": {
    "object": { /* the event-specific payload */ }
  }
}
```

<ResponseField name="id" type="string">
  Unique id for this delivery. A resend of the same logical event uses a brand-new id, not the original.
</ResponseField>

<ResponseField name="type" type="string">
  The event name. See [Event types](/webhooks/event-types) for the full list and what each one means.
</ResponseField>

<ResponseField name="api_version" type="string">
  Schema version of the payload. Currently always `2026-01-01`. If the schema ever evolves, the new version will be announced and the old one will keep working for a transition period.
</ResponseField>

<ResponseField name="created" type="number">
  Unix timestamp in **seconds**, marking when Boble emitted the event.
</ResponseField>

<ResponseField name="livemode" type="boolean">
  `true` in production, `false` in any test environment (including a test event you triggered manually outside production).
</ResponseField>

<ResponseField name="data.object" type="object">
  The event-specific payload. Its shape depends on `type`. The full layout for each event is in [Event types](/webhooks/event-types).
</ResponseField>

## The headers

Boble adds three headers on every delivery:

| Header            | Value                                                                                           |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| `Content-Type`    | `application/json`                                                                              |
| `User-Agent`      | `Boble-Webhook/1.0` — useful to filter Boble traffic in your access logs.                       |
| `Boble-Signature` | The HMAC signature of the body. See [Signature verification](/webhooks/signature-verification). |
| `Boble-Event-Id`  | Matches the `id` in the body. A resend of an event uses a **new** id.                           |

## Same buyer everywhere

All `user_promotion.*` events share the same nested `user` and `promotion` objects. This means once you've written code (or a Zap) to pull the buyer's email out of one event, the same path works on every event.

### `user` object

| Field                | Description                                                        |
| -------------------- | ------------------------------------------------------------------ |
| `id`                 | UUID of the buyer.                                                 |
| `email`              | Buyer's email address. May be `null` if the account was deleted.   |
| `firstname`          | First name. May be `null`.                                         |
| `lastname`           | Last name. May be `null`.                                          |
| `username`           | Username on Boble. May be `null`.                                  |
| `avatar`             | Full URL of the avatar. `null` when not set.                       |
| `preferred_language` | IETF code: `en`, `fr`, `es`. May be `null`.                        |
| `created_at`         | ISO-8601 timestamp of the buyer's account creation. May be `null`. |

### `promotion` object

| Field      | Description                                                    |
| ---------- | -------------------------------------------------------------- |
| `id`       | UUID of the promotion.                                         |
| `title`    | Title of the promotion.                                        |
| `type`     | `free`, `one_time`, or `subscription`.                         |
| `currency` | ISO-4217 code in uppercase: `EUR`, `USD`, `GBP`.               |
| `amount`   | Price in the promotion's currency. `null` for free promotions. |

## Same id across the lifecycle

For a given buyer/promotion pair, the `data.object.id` stays the same across every event. The first `access_granted` and a later `canceled` for the same access will carry the **same** `data.object.id`. Use it if you need to track a buyer's journey across events.

The top-level `id` (and the `Boble-Event-Id` header) is unique **per delivery** — a different one on every event, and a different one again on every resend.

## Avoiding double application

Boble does not retry automatically, but a manual **resend** dispatches a brand-new delivery with the same `data.object` but a different envelope `id`.

If your handler has side effects you don't want to apply twice (sending a receipt, granting an external role), key your idempotency on a value derived from `data.object` — typically `data.object.id` combined with the event `type`. That way, the original and any resend collapse into the same idempotency key.
