> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivolio.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivolio.com/_mcp/server.

# Errors

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

```json
{
  "detail": "Workspace name is required",
  "code": "validation_error",
  "errors": [{ "field": "name", "message": "Field required" }]
}
```

* **`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

| Status | What it means when ingesting                                                                                                               |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | No `X-Write-Key` header, or the key isn't recognized.                                                                                      |
| `403`  | The key is valid but wrong for this endpoint — most often a first-party SDK key used against `/ingest/integration`.                        |
| `404`  | The path doesn't exist. Check you haven't prefixed it with a version segment; the ingestion paths are `/track`, `/batch`, `/pixel`, `/id`. |
| `422`  | The event body failed validation. See below.                                                                                               |
| `429`  | A rate-limit budget was exceeded. See below.                                                                                               |
| `5xx`  | A 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.

> **Note**
>
> 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:

```json
{ "detail": "Rate limit exceeded (IP)" }
```

```json
{ "detail": "Rate limit exceeded (workspace)" }
```

The distinction drives the fix. `(IP)` means too many requests from one address
— batch your sends via [`/batch`](/documentation/guides/http-api), 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}`.

## Related

* [Sending events over HTTP](/documentation/guides/http-api) — the endpoints, the required
  fields, and the response envelope.
* [Authentication](/documentation/guides/authentication) — the difference between the two write
  key types that `403` is telling you about.