Errors

One consistent error shape across the whole API.

Every error the API returns shares a single envelope, so a client can always surface one human-readable message — even for validation failures:

1{
2 "detail": "Workspace name is required",
3 "code": "validation_error",
4 "errors": [{ "field": "name", "message": "Field required" }]
5}
  • detail — a human-readable message; always present.
  • code — a stable machine code you can branch on.
  • errors — present for validation failures: the offending fields and why.

Branch on code, not on detail. Messages are written for humans and may be reworded; codes are part of the contract.

Status codes in an ingestion context

StatusWhat it means when ingesting
401No X-Write-Key header, or the key isn’t recognized.
403The key is valid but wrong for this endpoint — most often a first-party SDK key used against /ingest/integration.
404The path doesn’t exist. Check you haven’t prefixed it with a version segment; the ingestion paths are /track, /batch, /pixel, /id.
422The event body failed validation. See below.
429A rate-limit budget was exceeded. See below.
5xxA collector-side failure. Retry with the same event_id — deduplication makes that safe.

Error responses never leak internal detail.

What a 422 means for events

An event body is rejected as a whole. The usual causes, in rough order of how often they bite:

  • A missing required field. event_id, event_name, event_time and anonymous_id are all required on every event. Omitting any one is a 422.
  • event_time in milliseconds. It must be Unix time in seconds (a float is fine). A millisecond value is not a validation error in itself, but it is the most common reason events land at an absurd timestamp.
  • An illegal event_name. It must match ^[a-zA-Z0-9_.]{1,128}$ — no spaces, no hyphens.
  • Oversized properties. The JSON-encoded object is capped at 64,000 bytes. Send identifiers, not payload dumps.
  • More than 500 events in a /batch request.

The errors array names the offending field, so log it rather than just the status.

A 422 is permanent. Retrying an identical body will fail identically — fix the payload instead. The SDKs encode this: 4xx responses other than 429 stop the retry loop immediately.

Rate limits and 429

There are two independent budgets, and they count different things:

  • Per IP — counts requests. A batch of 500 events costs one.
  • Per workspace — counts events. A batch of 500 events costs 500.

Which one you hit is stated in the response, so you don’t have to guess:

1{ "detail": "Rate limit exceeded (IP)" }
1{ "detail": "Rate limit exceeded (workspace)" }

The distinction drives the fix. (IP) means too many requests from one address — batch your sends via /batch, which collapses up to 500 events into a single request. (workspace) means genuine event volume, and batching won’t help at all; you’re sending more events than the workspace is provisioned for.

Unlike other 4xx responses, a 429 is retryable. Back off and resend with the same event_id — deduplication is keyed on (workspace, event_id), so a resent event is recorded once and comes back as {"duplicate": true}.

  • Sending events over HTTP — the endpoints, the required fields, and the response envelope.
  • Authentication — the difference between the two write key types that 403 is telling you about.