---
title: Errors
description: The error shape, the types, and the codes worth handling.
---

Every error has the same shape.

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "topic_not_found",
    "message": "No topic with that id on this workspace. Check the value against your topics list.",
    "param": "topic_id"
  }
}
```

- **`type`** — the broad category. Branch on this.
- **`code`** — the specific reason. Match on this when you need to.
- **`message`** — written for a human. Safe to log, not meant to be
  parsed.
- **`param`** — which field caused it, when one field did.

Match on `code`, never on `message`. Messages get reworded; codes do
not.

## Types

| Type | Status | Meaning |
|---|---|---|
| `authentication_error` | 401 | The key is missing, malformed or revoked |
| `permission_error` | 403 | The key is valid but may not do this |
| `invalid_request_error` | 400 | Something about the request is wrong |
| `not_found_error` | 404 | No such resource on this workspace |
| `rate_limit_error` | 429 | Too many requests. See [rate limits](/api/rate-limits) |

A `5xx` means the fault is ours. Retry with backoff.

## Codes worth handling

**`contact_unsubscribed`** — you tried to create or update a contact
who has opted out. Nothing was changed. This is not a failure to work
around: see [consent](/guide/consent#when-somebody-leaves-they-stay-gone).
Skip them and carry on.

**`publishable_key_not_allowed`** — a publishable key reached
something other than `/v1/subscribe`. Almost always a secret key that
should have been used on the server.

**`origin_not_allowed`** — a publishable key was used from a domain it
is not allowed on. Add the domain, or check you have not shipped a
staging key.

**`topic_not_found`** — no such topic on this workspace. Also returned
for an archived topic, so a caller cannot probe which ids ever
existed.

**`topic_not_on_key_brand`** — the key is scoped to one brand and that
topic belongs to another.

**`invalid_limit`** — `limit` was outside 1–100. We refuse rather than
quietly clamp, so you find out now instead of wondering why page
sizes look odd.

**`invalid_cursor`** — the cursor was not one we issued. Pass
`meta.next_cursor` back exactly as given; do not construct one.

**`team_inactive`** — the workspace cannot act right now, usually
billing. See [plans and billing](/guide/billing).

## Validation errors

A malformed body returns `invalid_request_error` with `param` naming
the field.

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_failed",
    "message": "email must be a valid email address.",
    "param": "email"
  }
}
```

Unknown fields are rejected rather than ignored. A typo'd `frist_name`
fails loudly instead of silently dropping the value.

## Retrying

Retry `429` and `5xx` with exponential backoff. Do not retry `4xx`
otherwise — the request will fail the same way every time.

Creating a contact is idempotent on the email address, so a retry
after a timeout merges rather than duplicating.