This page covers how the REST API reports problems: the error envelope, every status code, the per-event validation errors returned for a batch, the rate limits, and how a well-behaved client should retry.
The error envelope
When a request fails before any events are processed (bad auth, malformed body, a limit breach), the response body is a small, consistent object:
{ "success": false, "error": "Human-readable message" }Some errors also include an optional, stable, machine-readable code you can
switch on instead of matching the human-readable error string. It's additive —
clients that read only error / success are unaffected, and not every error
carries one:
{ "success": false, "error": "Rate limit exceeded: ...", "code": "rate_limited" }code | Status | Meaning |
|---|---|---|
rate_limited | 429 | Per-(org, IP) request-rate limit hit. |
ip_not_allowed | 403 | The key has an IP allowlist that the client IP isn't on. |
origin_not_allowed | 403 | The key has an Origin allowlist that the request Origin isn't on. |
The per-org 429 backstop and the plan-quota 429s carry no code — match
those on status + Retry-After. (See
Authentication for the per-key
IP / Origin allowlists behind the 403 codes.)
The 413 (payload too large) response carries the same shape plus two
machine-readable fields so a client can resize its batch without parsing the
prose message:
{
"success": false,
"error": "Request body too large: maximum 1048576 bytes",
"limit_bytes": 1048576,
"hint": "Split the batch into smaller requests (max 100 events / 1 MiB per call)."
}This is the /v1-batch 413 (1 MiB cap). /v1-profiles and /v1-consent parse
smaller bodies under a shared 256 KiB cap, so their 413 carries
limit_bytes: 262144 and a generic hint ("Reduce the request body size…").
Per-event failures are not in the envelope
The envelope above is for request-level failures. When the request itself
succeeds but individual events fail validation, the response is 200 with
success: false and a per-event errors array — see Per-event
errors below.
HTTP status codes
| Code | Meaning | Typical cause |
|---|---|---|
200 | OK | Request processed. May still report per-event failures in the body — inspect failed / errors. |
400 | Bad request | Content-Type isn't application/json, empty or invalid JSON body, missing batch array, or more than 100 events. |
401 | Unauthorized | Missing, malformed, empty, or invalid/expired Authorization header. |
403 | Forbidden | Valid key but wrong type (e.g. a read key used for ingestion — needs write/admin), or the key has an IP / Origin allowlist the request doesn't match (code: "ip_not_allowed" / "origin_not_allowed" — see Authentication). |
405 | Method not allowed | /v1-batch: any method other than POST/OPTIONS. /v1-consent: anything other than GET/POST/OPTIONS. (/v1-profiles returns 404 for an unmatched method/route instead.) |
413 | Payload too large | Request body exceeds 1 MiB. Returns the structured body shown above. |
429 | Rate limit / quota | Too many requests per minute, or your plan's event quota is exhausted. Most paths carry Retry-After (the monthly-limit 429 does not). See Rate limits. |
500 | Internal server error | An unexpected server-side error. Usually transient — retry with backoff, but a 500 that recurs on the same payload is likely deterministic, so stop after a few attempts. |
503 | Service unavailable | A transient backend issue (e.g. the usage check couldn't run). Carries Retry-After: 30. Retry after the delay. |
Request-level error messages
These are returned in the error field for request-level failures:
| Status | error message |
|---|---|
401 | Missing Authorization header |
401 | Invalid Authorization format. Use: Bearer <api_key> |
401 | Empty API key |
401 | Invalid or expired API key |
403 | Insufficient permissions: this operation requires a write or admin key |
400 | Content-Type must be application/json |
400 | Empty request body |
400 | Invalid JSON in request body |
400 | Invalid request: batch array is required |
400 | Batch too large: maximum 100 events per request |
429 | Rate limit exceeded: max 100 requests per minute per IP |
429 | Event quota exceeded. Upgrade your plan to resume ingestion. |
429 | Monthly event limit exceeded. Please upgrade your plan. |
413 | Request body too large: maximum 1048576 bytes |
503 | Service temporarily unavailable — usage check failed |
500 | Internal server error |
500 | Authentication failed (a thrown error while validating the API key) |
Response headers
Every response — success or error — carries these headers (they all route through the shared response helper):
| Header | Value | Notes |
|---|---|---|
X-Request-ID | a UUID, or your value echoed back | Correlation id. Send your own X-Request-ID (≤200 chars, [A-Za-z0-9._-]) and it's echoed back; otherwise the server mints one. Quote it when contacting support. |
API-Version | 1 | The API major version (see Versioning). |
X-Content-Type-Options | nosniff | — |
Referrer-Policy | no-referrer | — |
CORS preflights allow GET, POST, OPTIONS from any origin
(Access-Control-Allow-Origin: *), with Content-Type, Authorization, X-Request-ID request headers.
Versioning
The API is versioned by path prefix (/v1-*) and advertises its major
version in the API-Version response header (1 today). A future
breaking-change release would ship under a new prefix and bump the header;
non-breaking, additive changes (new optional fields, new code values) ship
within v1.
Per-event errors
When a batch is accepted but some events fail, the response is 200 with
success: false and an errors array. Each entry is "<messageId>: <reason>",
and the array lists at most the first ten failures:
{
"success": false,
"processed": 1,
"failed": 1,
"errors": [ "msg_002: anonymousId is required" ],
"duration_ms": 31
}The reasons you may see. Each is a stable, matchable string; raw database errors are never returned to clients (they go to server logs only):
| Reason | Cause |
|---|---|
Event is required | A null or empty event object appeared in the batch array. |
Event type is required | The event has no type. |
Invalid event type: <type> | type isn't one of track, identify, page, screen, alias, group. |
messageId is required | The event has no messageId. |
anonymousId is required | The event has no anonymousId. |
timestamp is required | The event has no timestamp. |
event name is required for track events | A track event with no event name. |
event name too long | A track event whose event name exceeds 200 characters. |
userId is required for alias events | An alias event with no userId. |
groupId is required for group events | A group event with no groupId. |
Invalid anonymousId | anonymousId is over 200 chars or matches the junk/sentinel denylist. |
Invalid userId | userId (when present) is over 200 chars or matches the denylist. |
Invalid previousId | An alias event's previousId is over 200 chars or matches the denylist (it now feeds a real merge). |
identity_resolution_failed | The event was well-formed but identity could not be resolved (a transient server-side condition). |
insert_failed | The event passed validation but the database write did not succeed. |
not_processed | The event was not processed (it never reached the write stage). |
processing_failed | Fallback when an event could not be stored and no more specific reason was available. |
Rate limits
Every endpoint enforces a per-minute request limit, scoped per (organization,
client IP). When you exceed it the response is 429 with headers that tell you
when to retry:
HTTP/1.1 429 Too Many Requests
Retry-After: 27
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0| Endpoint | Limit (per org + IP) |
|---|---|
POST /v1-batch | 100 / min |
GET /v1-profiles* (list · detail · search · events · identities) | 60 / min |
POST /v1-profiles/merge | 10 / min (separate bucket) |
POST & GET /v1-consent | 60 / min |
Retry-After is the seconds until the current minute window resets. The per-IP
429 carries code: "rate_limited".
Per-org backstop. On top of the per-IP limit, /v1-batch also enforces a
coarse 2000 requests per minute per organization (aggregated across all IPs),
so a leaked write key can't be amplified from many addresses. Exceeding it returns
429 with Retry-After + X-RateLimit-Limit: 2000. Note this per-org 429 body
is { "error": "Rate limit exceeded: max 2000 requests per minute per org" } — it
does not include success: false or a code field (unlike the per-IP 429),
so match it on status.
Two further 429s come from your plan's event quota rather than request rate:
- Monthly event limit reached —
Monthly event limit exceeded. Please upgrade your plan.This429returns the bare error envelope (noRetry-After); ingestion resumes when the monthly counter resets or you upgrade. - Ingestion paused —
Event quota exceeded. Upgrade your plan to resume ingestion., withRetry-After: 86400and anupgrade_url. This is a billing/plan state, not a transient throttle — backing off won't clear it; upgrading or the next reset will.
Retry policy
The official Web SDK treats 4xx responses as permanent — it drops the
event rather than retrying — and retries 5xx and network errors with
exponential backoff plus jitter, up to 3 attempts. This means the SDK treats
a 429 as permanent too: it does not read Retry-After or retry
rate-limited requests (only the server emits Retry-After). If you need
rate-limit retry today, call POST /v1-batch directly — tracked for a future SDK
release.
If you build your own client, a safe policy is:
2xx— done. Still checkfailed/errorsin the body.400/401/403/413— permanent. Fix the request (or the key); retrying the same payload will fail the same way.429— honour theRetry-Afterheader, then retry. For the quota-paused429(Retry-After: 86400), resolve the plan state instead of retrying.500/503/ network errors — transient. Retry with exponential backoff and jitter; honourRetry-Afteron503.
Keep the same messageId across retries — the server de-duplicates on it, so a
retried event won't be counted twice.
Next
- Event ingestion — the endpoint, event schema, and response shape.
- Profiles API & Consent API — the other REST endpoints (rate-limited here too).
- Event volume — batching strategy and staying under the limits at scale.
Last updated 2026-06-10