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" }
codeStatusMeaning
rate_limited429Per-(org, IP) request-rate limit hit.
ip_not_allowed403The key has an IP allowlist that the client IP isn't on.
origin_not_allowed403The 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

CodeMeaningTypical cause
200OKRequest processed. May still report per-event failures in the body — inspect failed / errors.
400Bad requestContent-Type isn't application/json, empty or invalid JSON body, missing batch array, or more than 100 events.
401UnauthorizedMissing, malformed, empty, or invalid/expired Authorization header.
403ForbiddenValid 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).
405Method 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.)
413Payload too largeRequest body exceeds 1 MiB. Returns the structured body shown above.
429Rate limit / quotaToo 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.
500Internal server errorAn 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.
503Service unavailableA 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:

Statuserror message
401Missing Authorization header
401Invalid Authorization format. Use: Bearer <api_key>
401Empty API key
401Invalid or expired API key
403Insufficient permissions: this operation requires a write or admin key
400Content-Type must be application/json
400Empty request body
400Invalid JSON in request body
400Invalid request: batch array is required
400Batch too large: maximum 100 events per request
429Rate limit exceeded: max 100 requests per minute per IP
429Event quota exceeded. Upgrade your plan to resume ingestion.
429Monthly event limit exceeded. Please upgrade your plan.
413Request body too large: maximum 1048576 bytes
503Service temporarily unavailable — usage check failed
500Internal server error
500Authentication 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):

HeaderValueNotes
X-Request-IDa UUID, or your value echoed backCorrelation 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-Version1The API major version (see Versioning).
X-Content-Type-Optionsnosniff
Referrer-Policyno-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):

ReasonCause
Event is requiredA null or empty event object appeared in the batch array.
Event type is requiredThe event has no type.
Invalid event type: <type>type isn't one of track, identify, page, screen, alias, group.
messageId is requiredThe event has no messageId.
anonymousId is requiredThe event has no anonymousId.
timestamp is requiredThe event has no timestamp.
event name is required for track eventsA track event with no event name.
event name too longA track event whose event name exceeds 200 characters.
userId is required for alias eventsAn alias event with no userId.
groupId is required for group eventsA group event with no groupId.
Invalid anonymousIdanonymousId is over 200 chars or matches the junk/sentinel denylist.
Invalid userIduserId (when present) is over 200 chars or matches the denylist.
Invalid previousIdAn alias event's previousId is over 200 chars or matches the denylist (it now feeds a real merge).
identity_resolution_failedThe event was well-formed but identity could not be resolved (a transient server-side condition).
insert_failedThe event passed validation but the database write did not succeed.
not_processedThe event was not processed (it never reached the write stage).
processing_failedFallback 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
EndpointLimit (per org + IP)
POST /v1-batch100 / min
GET /v1-profiles* (list · detail · search · events · identities)60 / min
POST /v1-profiles/merge10 / min (separate bucket)
POST & GET /v1-consent60 / 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 reachedMonthly event limit exceeded. Please upgrade your plan. This 429 returns the bare error envelope (no Retry-After); ingestion resumes when the monthly counter resets or you upgrade.
  • Ingestion pausedEvent quota exceeded. Upgrade your plan to resume ingestion., with Retry-After: 86400 and an upgrade_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 check failed / errors in the body.
  • 400 / 401 / 403 / 413 — permanent. Fix the request (or the key); retrying the same payload will fail the same way.
  • 429 — honour the Retry-After header, then retry. For the quota-paused 429 (Retry-After: 86400), resolve the plan state instead of retrying.
  • 500 / 503 / network errors — transient. Retry with exponential backoff and jitter; honour Retry-After on 503.

Keep the same messageId across retries — the server de-duplicates on it, so a retried event won't be counted twice.

Next

Last updated 2026-06-10

We use cookies for analytics — to understand how visitors use UMAP360 and improve the product. Essential cookies (session, forms) always run; analytics cookies wait for your call. See cookie policy.