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:
{
"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 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):
| 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:
{
"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_limitedand aRetry-Afterheader.
Retry guidance
- On HTTP 429, wait at least the number of seconds in
Retry-Afterbefore retrying. Do not retry in a tight loop — repeated bursts just consume the next window. - On HTTP 5xx, retry with exponential backoff.
- Reads (
GETand the consent resolve endpoint) are safe to retry as-is. - For consent state writes, retry a 409
version_conflictby re-reading the subject, merging against the returnedcurrentsnapshot, and writing again with the newversionasbaseVersion.