---
title: "Errors and rate limits"
description: "Error codes, HTTP statuses, request ids, rate-limit tiers, and retry guidance for the CookieBeam REST API."
url: "https://cookiebeam.com/docs/api-reference/errors-and-limits"
lang: "en"
---

# Errors and rate limits

Error codes, HTTP statuses, request ids, rate-limit tiers, and retry guidance for the CookieBeam REST API.

## Error envelope

Errors use the same envelope as successful responses. The `data` object carries a human-readable `error` message, a machine-readable `code`, an optional `details` value, and the `requestId` of the call:

```json
{
  "data": {
    "error": "API key is missing required scope(s): banners:write",
    "code": "missing_scope",
    "requestId": "9b2e5f0c-8d7a-4e0b-9c1d-2f3a4b5c6d7e"
  },
  "version": "v1"
}
```

Match on `code`; the wording of `error` may change. Validation errors add `details` describing each invalid field. Some endpoints add their own fields (a consent state conflict returns `current`).

## Request ids

Every `/api/v1` response carries an `X-Request-Id` header, and every error repeats it as `data.requestId`. Send your own `X-Request-Id` (a UUID) to correlate a call with your logs; any other value is ignored and a new id is generated. Quote the id when you contact support.

## Machine-readable description

The OpenAPI 3.1 document at [`/api/v1/openapi.json`](https://cookiebeam.com/api/v1/openapi.json) is generated from the same contracts the endpoints validate against. It lists every operation with its scopes, request and response schemas, and the error codes each status can carry.

## Error codes

Authentication and scope failures (see [Authentication](https://cookiebeam.com/docs/api-reference/authentication)):

| HTTP status | Code | Meaning |
| ----------- | ---- | ------- |
| 400 | `invalid_request` | The request body failed validation, or is not JSON |
| 401 | `invalid_key` | The API key is malformed, unknown, or has the wrong environment prefix |
| 401 | `revoked_key` | The API key has been revoked |
| 401 | `expired_key` | The API key has expired |
| 401 | `unauthorized` | The endpoint requires an API key and none was sent |
| 403 | `plan_required` | The team's plan does not include the required feature, or a plan limit was reached |
| 403 | `missing_scope` | The key lacks a required scope; the message names the missing scopes |
| 403 | `test_key_read_only` | A test key was used for a write or publish operation |
| 403 | `key_restricted` | A key restricted to specific banners was used to create a banner, to record a form consent proof without one of its banners, or to erase form consent proofs |
| 404 | `not_found` | The referenced banner or consent subject was not found, or is outside the key's banner restriction |
| 409 | `version_conflict` | A consent state write carried a stale `baseVersion`; the response includes the current snapshot |
| 409 | `idempotency_in_progress` | A request with the same `Idempotency-Key` is still running; retry after `Retry-After` seconds |
| 422 | `idempotency_key_reused` | The `Idempotency-Key` was already used with a different request |
| 413 | `payload_too_large` | The request body exceeded 64 KB (consent log) |
| 413 | `invalid_request` | The request body exceeded 20 KB (consent subject endpoints) |
| 429 | `rate_limited` | A rate limit was exhausted; see below |
| 500 | `internal_error` | Unexpected server error |
| 503 | `identity_unavailable` | Consent identity hashing is temporarily unavailable; retry later |

## Rate limits for API keys

API-key requests are limited per key over a fixed 15-minute window. The limit depends on the team's plan:

| Plan | Requests per 15 minutes |
| ---- | ----------------------- |
| Free | 50 |
| Starter | 200 |
| Professional | 500 |
| Business | 2,000 |

A plan the server cannot map to a tier falls back to the Free limit.

Responses that pass key authentication and reach the rate limiter carry these headers:

- `X-RateLimit-Limit` — the key's limit for the window.
- `X-RateLimit-Remaining` — requests left in the current window.
- `X-RateLimit-Reset` — when the window resets, as a Unix timestamp in milliseconds.
- `RateLimit` — the same state as an IETF structured field: `"default";r=<remaining>;t=<seconds until reset>`.
- `RateLimit-Policy` — the quota and window: `"default";q=<limit>;w=<window seconds>`.

This includes HTTP 429 rejections. It does not include requests rejected before rate limiting runs — entitlement, scope, and test-key failures (and malformed, revoked, or expired keys) return without these headers — and some server-error paths also skip them. Treat the headers as present on rate-limiter outcomes, not as a universal invariant.

When the limit is exhausted the API returns HTTP 429:

```json
{
  "data": {
    "error": "Rate limit exceeded",
    "code": "rate_limited",
    "requestId": "9b2e5f0c-8d7a-4e0b-9c1d-2f3a4b5c6d7e"
  },
  "version": "v1"
}
```

The response includes a `Retry-After` header with the number of seconds to wait before the next request.

## Other rate limits

Two limits apply independently of API keys:

- Requests to the banner endpoints that arrive without an API key (dashboard sessions and other browser traffic) are limited to 100 requests per 15 minutes per client IP.
- `POST /api/consent/log` (the unversioned banner ingestion route) is limited to 1,000 requests per minute per client IP.
- Failed authentication attempts are limited to 10 per minute per client IP; beyond that, further attempts return HTTP 429 with `rate_limited` and a `Retry-After` header.

## Retry guidance

- On HTTP 429, wait at least the number of seconds in `Retry-After` before retrying. Do not retry in a tight loop — repeated bursts just consume the next window.
- On HTTP 5xx, retry with exponential backoff.
- Reads (`GET` and the consent resolve endpoint) are safe to retry as-is.
- For consent state writes, retry a 409 `version_conflict` by re-reading the subject, merging against the returned `current` snapshot, and writing again with the new `version` as `baseVersion`.
