# Errors and limits

> Error format, codes, rate limits, size limits.

## Error format

Every error answer has the same JSON shape:

```json
{"error":{"code":"validation","message":"Please check the highlighted fields.","fields":{"email":"Enter a valid email address."}}}
```

| Key | Meaning |
|---|---|
| `code` | A stable, machine-readable code. Branch on this. |
| `message` | A human-readable text in the language of `Accept-Language` (`en` or `hu`). It can change, so don't parse it. |
| `fields` | Only for errors about the request's content: the problem per key, as human-readable text. Nested keys use dots (`fields.rendelesek_szama`, `content.html`, `requests.0`). |

The same request with `Accept-Language: hu`:

```json
{"error":{"code":"validation","message":"Ellenőrizd a megjelölt mezőket.","fields":{"email":"Adj meg érvényes e-mail-címet."}}}
```

A method the path doesn't take answers `405` with the `Allow` header:

```bash
curl -i -X PATCH "$ECHO_API/groups" -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" -d '{"name":"x"}'
```

```http
HTTP/1.1 405 Method Not Allowed
Allow: OPTIONS, GET, POST
Content-Type: application/json

{"error":{"code":"method_not_allowed","message":"This method is not allowed here."}}
```

A body that isn't valid JSON, or isn't one JSON object, answers `400`:

```bash
curl -X POST "$ECHO_API/groups" -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" -d '{"name":'
```

```json
{"error":{"code":"invalid_json","message":"The request body is not a valid JSON object."}}
```

A value of the wrong type is `422 validation` with the key in `fields`, for example `{"name":5}` gives `"fields":{"name":"Invalid value."}`.

## HTTP status codes

| Status | Meaning |
|---|---|
| `200`, `201`, `202`, `204` | Done; created; accepted for background work; done without a body. |
| `400` | The body isn't valid JSON or isn't one JSON object (`invalid_json`). |
| `401` | No valid token (`unauthenticated`). |
| `403` | The token can't do this (`forbidden`), or the address, the workspace or two-factor rules stop it. |
| `404` | Unknown ID or path (`not_found`). |
| `405` | The path exists but not with this method (`method_not_allowed`). The `Allow` header lists the methods it takes. |
| `409` | The request conflicts with the current state (see the codes below). |
| `413` | The body is too large (`too_large`). |
| `415` | The body isn't `application/json` (`unsupported_media_type`). |
| `422` | The content was understood but refused (`validation` and others below). |
| `429` | Rate limit reached (`rate_limited`). |
| `503` | A list or count took too long (`list_timeout`). Narrow the filter and try again. |

## Error codes

| Code | Status | When |
|---|---|---|
| `unauthenticated` | 401 | The token is missing, unknown or revoked, or its creator left the workspace or was disabled. |
| `ip_not_allowed` | 403 | The token is limited to other IP addresses. |
| `workspace_suspended` | 403 | The workspace is suspended. |
| `twofa_enforced` | 403 | The workspace requires two-factor authentication and the token's creator hasn't set it up. The token works again once they do. |
| `forbidden` | 403 | The token's access level or its creator's role doesn't allow this. |
| `not_found` | 404 | The ID doesn't exist in this workspace, or the path is unknown. |
| `method_not_allowed` | 405 | The path doesn't take this method; see the `Allow` header. |
| `invalid_json` | 400 | The body isn't valid JSON or isn't one JSON object. |
| `validation` | 422 | The request's content is wrong; see `fields`. |
| `unsupported_media_type` | 415 | Send `Content-Type: application/json`. |
| `too_large` | 413 | Over 1 MB (10 MB for an import). |
| `rate_limited` | 429 | See [Rate limits](#rate-limits). |
| `name_taken` | 409 | A group or field with this name exists. |
| `in_use` | 409 | A segment, campaign or workflow uses the group or segment you want to delete; the message names them. |
| `builtin_field` | 409 | Built-in fields can't be renamed or deleted. |
| `webhook_limit` | 409 | The workspace has 20 webhooks. |
| `unknown_tags` | 422 | Campaign content uses merge tags that don't exist. |
| `not_editable` | 409 | The campaign isn't in a state that allows this (not a draft, still sending, or content from another editor). |
| `editor_chosen` | 409 | A concurrent request chose another editor for the draft. |
| `recently_sent` | 409 | A campaign sent within 30 days can't be deleted yet. |
| `not_active` | 409 | The workflow is a draft (pause) or isn't active (add subscribers). |
| `list_timeout` | 503 | The list took too long. |

Treat unknown codes by their HTTP status: new codes may be added.

## Rate limits

| Bucket | Limit | Counts |
|---|---|---|
| General | 120 requests a minute per token | Every authenticated request, including batch envelopes and each request in a batch. |
| Import | 5 a minute per token | `POST /groups/{id}/import-subscribers`, which also counts in the general bucket. |

The limit is per calendar minute (it resets at the start of each minute), per token. Answers that count carry:

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | The bucket's limit. On an import answer it shows the import bucket. |
| `X-RateLimit-Remaining` | Requests left in this minute. |
| `Retry-After` | On `429` only: seconds until the next minute. |

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 18
X-Ratelimit-Limit: 120
X-Ratelimit-Remaining: 0

{"error":{"code":"rate_limited","message":"Too many requests with this API token. Wait a minute and try again, or send several requests in one batch."}}
```

Answers that don't count have no rate limit headers: a failed authentication, a request from a refused IP address, and `GET /openapi.yaml`.

Good practice: read `X-RateLimit-Remaining`, wait `Retry-After` seconds after a `429`, use [batch requests](https://docs.uptools.io/echo/developer-guide/batch-requests/) and the bulk import for many changes, and use separate tokens for separate systems (each token has its own limits).

## Other limits

| What | Limit |
|---|---|
| Request body | 1 MB; bulk import 10 MB |
| Page size (`limit`) | 1 to 100, default 25 |
| Subscriber activity page | 50 events |
| Bulk import | 1 to 50,000 rows per request |
| Batch | 1 to 50 requests |
| Add subscribers to a workflow | 1 to 100 per request |
| Test emails | 1 to 5 addresses per request; 50 per workspace in any 24 hours |
| Webhooks | 20 per workspace; up to 100 events per batched delivery; 10 seconds to answer |
| API tokens | 50 active per workspace; 20 IP addresses or ranges per token |
| Names | Webhook and token names up to 100 characters; campaign name and subject up to 255 |
