# Events and webhooks

> The events the CMS sends when content changes, what each one carries, how it is delivered, and which reach webhooks.

Source: https://docs.bifrostorbit.com/cms/developers/events/

When something changes in a space, such as a page going live or a menu being edited, the CMS records an event. Events let websites and other systems react to changes without asking the CMS over and over.

## Where events go

Every event goes to two places.

| Destination | What it gets | Format |
| --- | --- | --- |
| Orbit Exchange | Every event, in full | The event envelope below |
| Webhooks | Only events that have a webhook name, and only those the webhook listens for | A short signed message with the webhook name and a smaller set of data |

Orbit Exchange is the hub that connects Orbit products. It receives each event with everything the CMS knows about it.

Webhooks are addresses you register in **Configuration › Webhooks**. They are meant for websites that need to refresh a cache. See [Refresh the website when content changes](https://docs.bifrostorbit.com/cms/developers/webhooks/) to set one up.

## The envelope

Every event sent to Orbit Exchange has the same outer shape. Only `data` differs between events.

| Field | Type | What it holds |
| --- | --- | --- |
| `id` | string (UUID) | A unique ID for this event. Use it to spot duplicates. |
| `type` | string | The event name, such as `cms.page.published`. |
| `version` | integer | The version of this event's `data` shape. It goes up only if the shape changes in a way that could break a receiver. |
| `time` | string (ISO 8601, UTC) | When the change happened. |
| `source` | string | Always `cms` for these events. |
| `sequence` | integer | A number that goes up with every event the CMS sends. |
| `tenant.organisationId` | string | The organisation the change happened in. |
| `tenant.spaceId` | string or null | The space. |
| `tenant.environmentKey` | string or null | The environment key, such as `production`. |
| `tenant.regionKey` | string or null | The region key, such as `de`. |
| `tenant.localeCode` | string or null | The language code, such as `de-DE`. |
| `subject.resourceType` | string | What changed: `cms.page`, `cms.release`, `cms.redirect` or `cms.navigation`. |
| `subject.id` | string | The ID of the thing that changed. |
| `actor.type` | string | Who did it: `user`, `apiKey`, `agent`, `system` or `service`. |
| `actor.id` | string or null | Their ID. It is `null` for `system`, for example a scheduled release. |
| `data` | object | The details of the change. See the event families below. |

A full example, for a page going live:

```json
{
  "id": "3f2b6c1e-8d4a-4f7b-9a51-2c0e7d9b1a44",
  "type": "cms.page.published",
  "version": 1,
  "time": "2026-10-17T09:30:00.000Z",
  "source": "cms",
  "sequence": 48213,
  "tenant": {
    "organisationId": "6650f1c2a9e4b3d2c1f0e9a8",
    "spaceId": "6650f1c2a9e4b3d2c1f0e9b1",
    "environmentKey": "production",
    "regionKey": "germany",
    "localeCode": "de-DE"
  },
  "subject": { "resourceType": "cms.page", "id": "6650f1c2a9e4b3d2c1f0ea07" },
  "actor": { "type": "user", "id": "6650f1c2a9e4b3d2c1f0e011" },
  "data": {
    "contentId": "6650f1c2a9e4b3d2c1f0ea07",
    "contentType": "page",
    "name": "Hiking boots",
    "url": "/products/hiking-boots",
    "locale": "de-DE",
    "region": "germany",
    "environment": "production",
    "version": 7,
    "revisionId": "6650f1c2a9e4b3d2c1f0eb42",
    "publishedAt": "2026-10-17T09:30:00.000Z",
    "redirectFrom": null
  }
}
```

## Delivery guarantees

**Orbit Exchange: at least once.** Each event is kept until Orbit Exchange accepts it. If Orbit Exchange cannot be reached, the CMS tries again after 30 seconds, then waits twice as long each time, up to an hour between tries, for 12 tries in all (about five hours). Because a try can succeed even when the answer is lost, the same event can arrive more than once.

**Webhooks: once, when the change happens.** A webhook message is sent while the change is being saved. If your endpoint fails or does not answer within 10 seconds, the failure is shown in the webhook's **Deliveries**, but the message is not sent again. See [Retries and failures](https://docs.bifrostorbit.com/cms/developers/webhooks/) in the webhooks guide.

**Order.** Events are sent in the order the changes happen, but a retried event can arrive after newer ones. Sort by `sequence` when order matters. The number is shared across the whole platform, so gaps between your events are normal. Only the order means anything.

**Make receivers idempotent.** Handling the same event twice should do no harm.

- In Orbit Exchange events, keep the `id` of events you have handled and skip any you have seen.
- Webhook messages carry no event ID. Refreshing a cache twice is harmless, so most receivers need no extra work. If yours does more, combine `event`, `contentId` and `version`.
- Treat an event as a sign that something changed, not as the final state. If two changes arrive out of order, fetch the current content from the delivery API instead of trusting the older message.

## Event families

### Pages and entries

These events are about one item of content in one region and language. "Pages" here also covers entries without an address, such as global content. Those have `url` set to `null`.

Every page event carries these fields in `data`:

| Field | Type | What it holds |
| --- | --- | --- |
| `contentId` | string | The item's ID. |
| `contentType` | string | The content type key, such as `page`. |
| `name` | string | The item's name in the CMS. |
| `url` | string or null | The page address, such as `/products/hiking-boots`. `null` for entries without an address. |
| `locale` | string or null | The language code. |
| `region` | string or null | The region key. |
| `environment` | string or null | The environment key. |

Each event then adds its own fields.

| Event | Triggered when | Extra fields | Webhook |
| --- | --- | --- | --- |
| `cms.page.published` | A page is published, or its changes are, by hand, on a schedule or in a release. | `version`, `revisionId`, `publishedAt`, and `redirectFrom`, the old address if the address changed and a redirect was added. | `content.published` |
| `cms.page.unpublished` | A page is taken offline. | `revisionId` | `content.unpublished` |
| `cms.page.rolledBack` | A page goes back to an earlier version. | `toVersion`, `revisionId`, and `republished`, which is `true` if the page was live and the older version went live too. | `content.published` |
| `cms.page.promoted` | A page is copied into another environment. | `sourceContentId`, `fromEnvironment`, `toEnvironment`, and `created`, which is `true` if the page was new there. | `content.published`, to webhooks on the environment it was copied into |
| `cms.page.trashed` | A page is moved to the bin. | None | `content.deleted` |
| `cms.page.restored` | A page comes back from the bin. | None | None |
| `cms.page.purged` | A page in the bin is erased for good. | None | None |

An unpublished event, as `data`:

```json
{
  "contentId": "6650f1c2a9e4b3d2c1f0ea07",
  "contentType": "page",
  "name": "Hiking boots",
  "url": "/products/hiking-boots",
  "locale": "de-DE",
  "region": "germany",
  "environment": "production",
  "revisionId": "6650f1c2a9e4b3d2c1f0eb57"
}
```

### Releases

`cms.release.published` is sent once a release has run, by hand or on its schedule. Each page in the release also sends its own `cms.page.published` or `cms.page.unpublished` event first.

| Field | Type | What it holds |
| --- | --- | --- |
| `releaseId` | string | The release's ID. |
| `name` | string | The release name. |
| `published` | array of strings | IDs of the items that went live. |
| `unpublished` | array of strings | IDs of the items taken offline. |
| `failed` | array of objects | Items that could not be published, each with `contentId` and `reason`. |
| `scheduled` | boolean | `true` if the release ran on its schedule. |

```json
{
  "releaseId": "6650f1c2a9e4b3d2c1f0ec10",
  "name": "Autumn offers",
  "published": ["6650f1c2a9e4b3d2c1f0ea07", "6650f1c2a9e4b3d2c1f0ea19"],
  "unpublished": [],
  "failed": [],
  "scheduled": true
}
```

Webhook name: `release.published`.

### Menus

`cms.navigation.changed` is sent when a menu is created, changed or deleted.

```json
{ "navigationId": "6650f1c2a9e4b3d2c1f0ed21", "key": "main", "action": "updated" }
```

`action` is `created`, `updated` or `deleted`. Webhook name: `navigation.changed`.

### Redirects

`cms.redirect.changed` is sent when a redirect is created, changed or removed, including redirects the CMS adds itself when a page address changes.

```json
{
  "redirectId": "6650f1c2a9e4b3d2c1f0ee33",
  "action": "created",
  "source": "/products/hiking-boot",
  "destination": { "type": "content", "url": null, "contentId": "6650f1c2a9e4b3d2c1f0ea07" },
  "statusCode": 301,
  "origin": null
}
```

`destination.type` is `url` for a fixed address, or `content` for a page in the CMS. Redirect events go to Orbit Exchange only. They do not reach webhooks.

### Media and other changes

Uploading or changing media, and editing settings, schemas or other screens, does not send an event. Only the changes listed above do.

## Webhook messages

A webhook gets a smaller message than Orbit Exchange. The body is JSON with three fields:

| Field | Type | What it holds |
| --- | --- | --- |
| `event` | string | The webhook name, such as `content.published`, or `webhook.test` for a test. |
| `sentAt` | string (ISO 8601, UTC) | When the message was sent. |
| `data` | object | The fields below. |

| Webhook event | `data` fields |
| --- | --- |
| `content.published`, `content.unpublished`, `content.deleted` | `contentId`, `url`, `name`, `locale`, `region`, and `version` when the page was published |
| `release.published` | `releaseId`, `name`, `published` |
| `navigation.changed` | `navigationId`, `key` |
| `webhook.test` | `webhook`, the webhook's name |

```json
{
  "event": "content.published",
  "sentAt": "2026-10-17T09:30:00.412Z",
  "data": {
    "contentId": "6650f1c2a9e4b3d2c1f0ea07",
    "url": "/products/hiking-boots",
    "name": "Hiking boots",
    "locale": "de-DE",
    "region": "germany",
    "version": 7
  }
}
```

The headers and how to check the signature are in [Refresh the website when content changes](https://docs.bifrostorbit.com/cms/developers/webhooks/).

## What the SDK refreshes

The SDK's `createRevalidateHandler` turns each webhook message into the cache tags to refresh. It only works if the client is set up with `cache: { tags: true }`. See [The SDK](https://docs.bifrostorbit.com/cms/developers/sdk/).

| Webhook event | Tags refreshed |
| --- | --- |
| `content.published`, `content.unpublished`, `content.deleted` for a page with an address | The page, `cms:page:{region}:{url}`, plus lists (`cms:list`), the sitemap (`cms:sitemap`) and everything read with references (`cms:references`). Without a region, everything (`cms`). |
| The same events for an entry without an address | Global content (`cms:global`), lists (`cms:list`), popups (`cms:popups`), banners (`cms:banners`) and everything read with references (`cms:references`) |
| `navigation.changed` | That menu, `cms:nav:{key}` |
| `release.published` | Lists, the sitemap and everything read with references. The pages themselves are refreshed by their own events. |
| `webhook.test` | Nothing |
| Anything else | Everything (`cms`) |

For example, publishing `/products/hiking-boots` in region `germany` refreshes `cms:page:germany:/products/hiking-boots`, `cms:list`, `cms:sitemap` and `cms:references`.

Not using the SDK? [Connect a website without Next.js](https://docs.bifrostorbit.com/cms/developers/connect-any-website/) shows how to refresh your own cache.

## Every event

The table below is built from the live list of events, so it is always current.

| Event | When | Webhook event |
| --- | --- | --- |
| `cms.page.published` (version 1) | A page or entry went live, or its changes did. | `content.published` |
| `cms.page.unpublished` (version 1) | A page or entry was taken offline. | `content.unpublished` |
| `cms.page.rolledBack` (version 1) | A page went back to an earlier version. | `content.published` |
| `cms.page.promoted` (version 1) | A page was copied live into another environment. | `content.published` |
| `cms.page.trashed` (version 1) | A page or entry was moved to the bin. | `content.deleted` |
| `cms.page.restored` (version 1) | A page or entry came back from the bin. | Orbit Exchange only |
| `cms.page.purged` (version 1) | A trashed page was erased for good. | Orbit Exchange only |
| `cms.release.published` (version 1) | A release went live with all its pages. | `release.published` |
| `cms.redirect.changed` (version 1) | A redirect was created, changed or removed. | Orbit Exchange only |
| `cms.navigation.changed` (version 1) | A menu changed. | `navigation.changed` |
