---
title: "Consent"
description: "Record consent choices and sync consent state across devices with the CookieBeam REST API."
url: "https://cookiebeam.com/docs/api-reference/consent"
lang: "en"
---

# Consent

Record consent choices and sync consent state across devices with the CookieBeam REST API.

Two groups of endpoints handle consent: the unversioned ingestion endpoint that the banner runtime posts to when a visitor makes a choice, and key-authenticated endpoints that read and write a subject's consent state across devices.

## How the banner records a consent choice

```text
POST /api/consent/log
```

This unversioned route is what the generated banner script calls. It is not part of the key-authenticated v1 API: requests carry no API key, are rate limited per client IP (1,000 requests per minute), and are accepted cross-origin so the banner can post from your domain. Each request is validated against the banner's authorized domain list — a post from a domain the banner is not configured for is rejected with HTTP 403. Scripts published from core 0.3.11 also append a per-banner token as the `cbt` query parameter; a token that does not belong to the banner is rejected with HTTP 401.

The payload uses the **public** Banner ID (the one in the loader script URL), a `preferences` object of category booleans, a `method` (`consent_given`, `consent_changed`, `consent_default`, `accept_all`, or `accept_necessary`), and an ISO 8601 `timestamp`. Optional fields cover geo resolution (`country`, `region`, `matchedRuleId`), the privacy signal detected at choice time, and US opt-out `purposeChoices`.

Server-side, the route resolves the public ID to the banner, clamps implausible client timestamps, and stores a privacy-minimized record: raw IP and user agent are never written, and the referrer is redacted to the banner's configured telemetry level. A successful write returns HTTP 200 with `success: true`, the stored `consentLogId`, and — for explicit visitor choices — a short-lived `managePreferences` URL the banner can offer for later changes. Explicit choices also trigger the `consent.given` [webhook event](https://cookiebeam.com/docs/api-reference/webhooks); machine-written default-state rows do not.

You normally never call this route yourself — the published banner script handles it, including the exact payload shape, which evolves with the runtime. It is documented here so you can recognize and troubleshoot the banner's network traffic.

## Record a consent choice with an API key

```text
POST /api/v1/consent/log
```

Requires the `consent:write` scope. Use it to record consent collected outside the banner script, for example by a [mobile app](https://cookiebeam.com/docs/install/mobile) or your own server.

| Field | Type | Notes |
| ----- | ---- | ----- |
| `bannerId` | string (UUID) | The banner's **internal** ID, as returned by the banners endpoints and as `banner.id` by the mobile config endpoint |
| `consentChoices` | object | Category names mapped to booleans |
| `language` | string | Optional language tag |
| `timestamp` | string (ISO 8601) | Optional time of the choice |
| `platform` | string | Optional; one of `web`, `ios`, `android`, `react-native`, `flutter`. Stored with the record |
| `sdkVersion` | string | Optional SDK version, letters, digits and `.+_-` only. Stored with the record |

A successful write returns HTTP 201 with the stored record's `id`. A banner that does not exist, belongs to another team or is inactive returns HTTP 404 with the code `not_found`.

## Cross-device consent state

The subject endpoints store a visitor's consent state server-side so it can follow them across devices. They identify a subject by three fields, sent in every request body:

| Field | Type | Notes |
| ----- | ---- | ----- |
| `bannerId` | string (UUID) | The banner's **public** Banner ID — the one that appears in the loader script URL, not the internal ID returned by the banners endpoints |
| `namespace` | string | Optional partitioning label, 1–64 characters; defaults to `default` |
| `subjectId` | string | Your own opaque identifier for the visitor, 1–256 characters. Do not use an email address; the request is rejected if the value contains `@` |

Request bodies are limited to 20 KB. These endpoints require a plan that includes cross-device consent; other plans receive a `plan_required` error (HTTP 403).

### Read a subject's state

```text
POST /api/v1/consent/subjects/resolve
```

Requires the `consent:read` scope.

```json
{
  "bannerId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "namespace": "default",
  "subjectId": "account-81f3c2"
}
```

Returns the stored snapshot, or `found: false` when none exists:

```json
{
  "data": {
    "found": true,
    "consentState": { "schemaVersion": 2, "categories": ["necessary", "analytics"] },
    "protocolVersion": 1,
    "version": 3,
    "clientUpdatedAt": "2026-08-13T11:58:00.000Z",
    "updatedAt": "2026-08-13T11:58:01.000Z",
    "expiresAt": "2026-11-13T11:58:01.000Z"
  },
  "version": "v1"
}
```

### Write a subject's state

```text
PUT /api/v1/consent/subjects/state
```

Requires the `consent:write` scope and a live key. The body adds three fields to the subject reference:

| Field | Type | Notes |
| ----- | ---- | ----- |
| `baseVersion` | integer | The `version` you last read, or `0` for a first write. A mismatch means another writer updated the record first |
| `consentState` | object | The canonical consent state (below) |
| `expiresAt` | string (ISO 8601) | Optional expiry for the stored record |
| `protocolVersion` | integer | Optional; currently `1` |

The canonical `consentState` object:

| Field | Type | Notes |
| ----- | ---- | ----- |
| `schemaVersion` | integer | Must be `2` |
| `categories` | string[] | Accepted category names; at most 50 entries, 1–100 characters each |
| `revision` | integer | Configuration revision the choice was made against, 0–1,000,000 |
| `acceptType` | string | `all`, `custom`, or `necessary` |
| `consentId` | string (UUID) | Identifier of the consent record |
| `consentTimestamp` | string (ISO 8601) | When the first choice was made |
| `lastConsentTimestamp` | string (ISO 8601) | When the latest choice was made; cannot precede `consentTimestamp` |
| `languageCode` | string | 1–20 characters |
| `services` | object | Optional map of category names to accepted service names |
| `jurisdictions` | string[] | Optional jurisdiction tags |
| `gpcRespected` | boolean | Optional; records whether a Global Privacy Control signal was honored |
| `usOptOutChoices` | object | Optional booleans: `saleSharing`, `targetedAdvertising`, `sensitiveData`, `profiling` |
| `isPending` | boolean | Optional; marks a pending opt-in default |
| `isDefaultState` | boolean | Optional; marks a jurisdiction default rather than an explicit choice |
| `collectedVia` | string | Optional; one of `banner`, `preferences`, `api`, `default_optin`, `default_optout`, `default_notice`, `hidden_autogrant` |

A successful write returns HTTP 200 with `success: true` plus the stored snapshot in the same shape as the resolve response.

If `baseVersion` does not match the stored version, the write is rejected with HTTP 409 and the code `version_conflict`; the response includes the current snapshot so you can merge and retry:

```json
{
  "data": {
    "error": "Consent version conflict",
    "code": "version_conflict",
    "current": {
      "consentState": { "schemaVersion": 2, "categories": ["necessary"] },
      "protocolVersion": 1,
      "version": 4,
      "clientUpdatedAt": "2026-08-13T11:59:00.000Z",
      "updatedAt": "2026-08-13T11:59:01.000Z",
      "expiresAt": "2026-11-13T11:59:01.000Z"
    }
  },
  "version": "v1"
}
```

### Delete a subject's state

```text
DELETE /api/v1/consent/subjects/state
```

Requires the `consent:write` scope and a live key. Send only the subject reference fields (`bannerId`, `namespace`, `subjectId`). The response reports how many records were deleted:

```json
{
  "data": {
    "success": true,
    "deleted": 1
  },
  "version": "v1"
}
```

Use this to honor a visitor's request to forget their stored consent state.

### Subject endpoint errors

| HTTP status | Code | Cause |
| ----------- | ---- | ----- |
| 400 | `invalid_request` | The body failed validation or is not valid JSON |
| 403 | `plan_required` | The team's plan does not include cross-device consent |
| 404 | `not_found` | No active banner with that public Banner ID on this team |
| 409 | `version_conflict` | `baseVersion` did not match the stored version (write only) |
| 413 | `invalid_request` | The body exceeded 20 KB |
| 500 | `internal_error` | Unexpected server error |
| 503 | `identity_unavailable` | Identity hashing is temporarily unavailable; retry later |
