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

# Webhook events and payloads

> Every event an endpoint can subscribe to, which ones Brudcast delivers, and the exact headers, envelope and data your handler receives.

This page answers one question: what will hit my endpoint? Each delivery is an HTTP `POST` from
Brudcast to your URL, with a JSON envelope whose `data` object depends on the event type. You choose
the types under **Subscribe to events** in **Developers > Webhooks**, or in the `events` array on
the platform API (`https://core-service.prod.brudcast.com/api/v1/user`, scope `webhooks:write`).

<Warning>
  Brudcast currently sends deliveries for `push.token_invalidated` and `test.event` only. You can
  subscribe to the other event types, but no deliveries are sent for them. Don't build a workflow
  that waits for one. Use the API calls under
  [Events that aren't delivered](#events-that-arent-delivered) instead.
</Warning>

## Headers

| Header                  | Value                                                                                                                                                      |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`          | `application/json`                                                                                                                                         |
| `X-Webhook-Signature`   | Lowercase hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret. No prefix. See [Verify signatures](/developers/webhooks/verifying-signatures) |
| `X-Webhook-Delivery-Id` | The delivery's ID. The same value as `delivery_id` in the body                                                                                             |
| `X-Webhook-Event`       | The event type. The same value as `event` in the body                                                                                                      |
| Your custom headers     | Each header you configured on the endpoint. A custom header with the same name as one above replaces it                                                    |

HTTP header names aren't case-sensitive. Many frameworks give them to you in lowercase, for example
`x-webhook-signature`.

## Envelope

Every event, whatever its type, arrives in the same four fields.

<ResponseField name="delivery_id" type="string" required>
  The delivery's ID, a UUID. It stays the same across automatic retries and manual resends. Use it
  to skip deliveries you've already processed.
</ResponseField>

<ResponseField name="event" type="string" required>
  The event type, for example `push.token_invalidated`.
</ResponseField>

<ResponseField name="data" type="object" required>
  The event's data. Its fields are listed per event below.
</ResponseField>

<ResponseField name="timestamp" type="integer" required>
  Unix time, in seconds, when Brudcast recorded the delivery. This isn't the time of the current
  attempt: retries and manual resends carry the original value.
</ResponseField>

The body Brudcast sends is compact JSON, with no spaces or line breaks. The examples on this page
are formatted to make them easier to read. Always verify the signature against the bytes you
received, not against JSON you've parsed and serialized again.

## Delivered events

These two are the only event types Brudcast sends today.

### `push.token_invalidated`

Firebase Cloud Messaging reported a device token as no longer registered, as registered to a
different Firebase project, or as malformed. Brudcast expires the device so campaigns stop
addressing it, then sends this event. It fires once per device: a later report about a device that's
already expired sends nothing.

<ResponseField name="tokenId" type="string" required>
  The ID of the device record on the contact. It matches the `id` of one entry in the contact's
  `pushNotifications` array.
</ResponseField>

<ResponseField name="contactId" type="string" required>
  The contact the device belongs to.
</ResponseField>

<ResponseField name="applicationId" type="string" required>
  The push application the token was registered with.
</ResponseField>

<ResponseField name="platform" type="string" required>
  The device platform: `ios`, `android` or `web`.
</ResponseField>

<ResponseField name="reason" type="string | null" required>
  The reason Firebase gave, as text. The wording comes from Firebase and can vary, so don't match
  on it.
</ResponseField>

<ResponseField name="occurredAt" type="string | null" required>
  When the token was rejected, as an ISO 8601 timestamp.
</ResponseField>

```json theme={"system"}
{
  "delivery_id": "01a08ff7-2a11-7d52-8e03-5b7c9d0e4f12",
  "event": "push.token_invalidated",
  "data": {
    "tokenId": "01a08b3c-5e21-7f40-8a62-3d9e1c7b4a05",
    "contactId": "01a07d2e-9b14-7c83-b5f1-0e6a2d8c4f37",
    "applicationId": "01a05f61-3c7d-7e29-9f08-4b1a6e2d7c93",
    "platform": "android",
    "reason": "Requested entity was not found.",
    "occurredAt": "2026-09-11T10:15:29.874Z"
  },
  "timestamp": 1789121730
}
```

A typical handler looks up the device by `tokenId` and stops sending to it from your own systems
too. For example, it deletes the token from your user's profile so the app registers a fresh one on
its next launch.

### `test.event`

You selected **Test Webhook** on the endpoint, or called `POST /webhook-endpoints/{id}/test`. It
goes to that endpoint only. It's sent whether or not the endpoint subscribes to `test.event`, and
even while the endpoint is disabled.

<ResponseField name="message" type="string" required>
  Always `This is a test webhook delivery`.
</ResponseField>

<ResponseField name="timestamp" type="string" required>
  When the test was requested, as an ISO 8601 timestamp. Unlike the envelope's `timestamp`, this
  one is a string.
</ResponseField>

```json theme={"system"}
{
  "delivery_id": "01a08ff7-17d0-7c3e-9b41-2c8f5e7d1a90",
  "event": "test.event",
  "data": {
    "message": "This is a test webhook delivery",
    "timestamp": "2026-09-11T10:15:30.512Z"
  },
  "timestamp": 1789121730
}
```

Because `test.event` reaches any endpoint on demand, it's the way to prove your URL, signature check
and `delivery_id` handling work before you rely on them.

## Events that aren't delivered

The dashboard lists these, and the API accepts them in `events`, but Brudcast doesn't send
deliveries for them, so they have no payload to document. The right-hand column gives the public API
call that answers the same question.

| Group    | Event types                                                              | Use instead                                                                                                                                                   |
| -------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Email    | `email.delivered`, `email.bounced`, `email.deferred`                     | `GET /messages`, filtered by `status` (`delivered`, `bounced` or `deferred`). Scope `messages:read`. See [Message status](/developers/sending/message-status) |
| Email    | `email.opened`, `email.clicked`, `email.complained`, `email.marked_spam` | `GET /messages/{jobId}`, whose `engagementEvents` holds `opened`, `clicked` and `complained` events. Scope `messages:read`                                    |
| Email    | `email.unsubscribed`, `email.dropped`, `email.rejected`                  | No public API equivalent                                                                                                                                      |
| Contact  | `contact.subscribed`, `contact.unsubscribed`, `contact.updated`          | `GET /contacts?sortBy=updatedAt&sortOrder=desc` returns the most recently changed contacts first. Scope `contacts:read`                                       |
| Campaign | `campaign.sent`, `campaign.completed`                                    | `GET /campaigns/{id}` and read `status`: it moves from `sending` to `sent`, then `completed`. Scope `campaigns:read`                                          |

## Subscribing with the API

The `events` array on `POST /webhook-endpoints` and `PATCH /webhook-endpoints/{id}` accepts any
event type on this page, including `test.event`. On `PATCH`, the array replaces the endpoint's whole
subscription. To add one event, send the full list.

The dashboard groups events as **Email Events**, **Contact Events**, **Campaign Events** and **Push
Events**. It doesn't list `test.event`, because test deliveries are sent whatever the subscription.

## Where the payload appears in Brudcast

The delivery log stores each delivery's `data` object. It appears as **Request Body** in the
delivery details in the dashboard, and as `payload` on delivery records from
`GET /webhook-deliveries`. Neither shows the envelope around it.

## Related

<Columns cols={2}>
  <Card title="Verify signatures" icon="shield-check" href="/developers/webhooks/verifying-signatures">
    Check `X-Webhook-Signature` before you trust the body.
  </Card>

  <Card title="Deliveries and retries" icon="refresh-cw" href="/developers/webhooks/deliveries-and-retries">
    What counts as delivered, the backoff schedule and the delivery log.
  </Card>

  <Card title="Create an endpoint" icon="plus" href="/developers/webhooks/create-an-endpoint">
    Register a URL, pick events and save the secret.
  </Card>

  <Card title="Register device tokens" icon="smartphone" href="/channels/push/register-device-tokens">
    Where the tokens in `push.token_invalidated` come from.
  </Card>
</Columns>
