---
title: "Webhooks"
description: "Receive CookieBeam events as signed HTTP POST requests on your own endpoints."
url: "https://cookiebeam.com/docs/api-reference/webhooks"
lang: "en"
---

# Webhooks

Receive CookieBeam events as signed HTTP POST requests on your own endpoints.

Webhooks deliver CookieBeam events to an HTTPS endpoint you control. Webhooks are a plan-gated feature, and endpoint management is currently restricted: endpoints are configured for your team by CookieBeam rather than self-served in the dashboard. Each endpoint subscribes to a set of event types and has a signing secret, which starts with `whsec_`.

There are no public REST endpoints for webhook management. The `webhooks:read` and `webhooks:write` API key scopes exist on keys but currently gate nothing.

## Event types

Six event types are currently emitted:

- `consent.given` — a visitor recorded an explicit consent choice
- `scan.completed` — a cookie scan finished
- `scan.policy_update_needed` — scan results require a policy review
- `dsar.request_submitted` — a data subject access request was submitted
- `banner.published` — a banner configuration was published
- `compliance.report_generated` — a compliance report was generated

The endpoint configuration form may list additional event types that are registered but not yet emitted by any code path — subscribing to one of those is accepted but delivers nothing. Subscribe only to the six above.

## Delivery format

Each delivery is an HTTP POST with a JSON envelope:

```json
{
  "event": "consent.given",
  "data": {
    "consentLogId": "9b2f7c14-8b3a-4e21-9d5c-1a2b3c4d5e6f",
    "bannerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "consentGiven": { "necessary": true, "analytics": false },
    "pageUrl": "https://example.com/pricing",
    "timestamp": "2026-08-13T12:00:00.000Z"
  },
  "timestamp": "2026-08-13T12:00:01.000Z",
  "deliveryId": "d290f1ee-6c54-4b01-90e6-d701748f0851"
}
```

The `data` payload is the example above for `consent.given`; other event types carry their own payloads. For `dsar.request_submitted`, `data.receivedAt` is the ISO 8601 time the submission was received and starts the deadline clock. `data.submittedAt` is the later verification time. The payload also includes `deadlineAt` and `slaPolicy`.

Request headers:

| Header | Value |
| ------ | ----- |
| `Content-Type` | `application/json` |
| `User-Agent` | `CookieBeam-Webhooks/1.0` |
| `X-CookieBeam-Timestamp` | Unix time in seconds when the delivery was signed |
| `X-CookieBeam-Signature-V1` | One or more `v1=<hex>` entries, comma-separated: HMAC-SHA256 of `timestamp + "." + raw body`, keyed with an endpoint secret |
| `X-CookieBeam-Event` | The event type, for example `consent.given` |
| `X-CookieBeam-Delivery` | The delivery ID, matching `deliveryId` in the body |
| `X-CookieBeam-Signature` | Legacy, only for endpoints created before timestamped signatures (see below): HMAC-SHA256 hex digest of the raw body |

## Verify the signature

Verify `X-CookieBeam-Signature-V1` before you trust a delivery:

1. Read `X-CookieBeam-Timestamp`. Reject the request if it is missing, not a whole number, or more than 300 seconds away from your clock. This stops a captured request from being replayed later.
2. Build the signed string: the timestamp, a dot, then the raw request body exactly as received.
3. For each secret you accept (normally one; two while you rotate), compute the HMAC-SHA256 hex digest of that string.
4. Split `X-CookieBeam-Signature-V1` on commas and keep the entries that start with `v1=`. Accept the request if any entry equals one of your digests, compared in constant time. Otherwise reject it.

```
timestamp = header("X-CookieBeam-Timestamp")
if timestamp is not digits or abs(now_seconds() - int(timestamp)) > 300: reject
signed = timestamp + "." + raw_body
candidates = [e[3:] for e in header("X-CookieBeam-Signature-V1").split(",") if e starts with "v1="]
for secret in accepted_secrets:
    expected = hex(hmac_sha256(key = secret, message = signed))
    if any(constant_time_equal(expected, c) for c in candidates): accept
reject
```

Use the raw body as received. Parsing and re-serializing the JSON can change the bytes and produce a false mismatch.

The `v1=` prefix names the signature scheme. A later scheme would be added as another prefix in the same header, so ignore entries you do not recognise rather than failing on them.

### The legacy `X-CookieBeam-Signature` header

Endpoints created before timestamped signatures still receive `X-CookieBeam-Signature`, unchanged in name and format: the HMAC-SHA256 hex digest of the raw body alone, keyed with the endpoint secret. Receivers that verify it today keep working.

It has no timestamp, so it does not protect against replays. It is sent for each existing endpoint until that endpoint's end date, which is announced by email at least six months ahead, with a reminder one month before. Once your receiver verifies `X-CookieBeam-Signature-V1`, you can ask CookieBeam to switch it off earlier for that endpoint; switching it off cannot be undone. Endpoints created now receive only the v1 headers.

If your receiver checks both during the move, check them separately. Never accept a request on the legacy digest after the v1 check failed: that would let an old captured request through.

## Signing secrets

The signing secret is shown once, when the endpoint is created or the secret is rotated, and is stored encrypted. It cannot be displayed again; if you lose it, ask CookieBeam to rotate it.

Rotating creates a new secret. You can keep the old one signing for up to 72 hours so you can update your receiver without missing events. During that overlap `X-CookieBeam-Signature-V1` carries one `v1=` entry per secret (the new one first), and the legacy header, where it is still sent, stays signed with the old secret until the overlap ends.

## Duplicates

A delivery can arrive more than once, for example when your endpoint answered after the 10-second timeout. Use `X-CookieBeam-Delivery` (or `deliveryId` in the body) to ignore deliveries you have already processed.

## Receiving requirements

Your endpoint must:

- Use HTTPS with a publicly resolvable host. Private, loopback, and reserved IP addresses are rejected when the endpoint is created and again at delivery time.
- Answer directly. Redirects are refused and count as failed deliveries.
- Respond within 10 seconds. The response body is recorded only up to 4,000 characters.
- Return a 2xx status to acknowledge a delivery. Any other outcome follows the retry policy below.

## Retries and disabling

Deliveries are queued and sent by a background runner that processes pending deliveries every few minutes. Each delivery is claimed by one run at a time, so overlapping runs do not send it twice.

A failed delivery is tried up to 8 times in total over about 27 hours: immediately, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. Each delay includes random variation so that queued deliveries do not all arrive at once when your endpoint recovers.

Network errors, timeouts, HTTP 5xx, 408, and 429 responses are retried. Other 4xx responses are treated as permanent rejections and are not retried, because the endpoint rejected the request itself.

An endpoint that accumulates 10 consecutive delivery failures is disabled automatically, and the team owner is notified by email. Ask CookieBeam to re-enable the endpoint once it is healthy again.
