# Errors and limits

## Error shape

Failed requests return JSON:

```json
{
  "code": "content_blocked",
  "error": "message blocked by the content gate",
  "hint": "Remove promotional phrasing or send on the broadcast route after DOI."
}
```

`hint` is safe to show in a UI. Do not parse `error` for control flow — use `code`.

## HTTP status

| Status | Typical meaning |
| --- | --- |
| 202 | Send accepted (queued) |
| 400 | Validation, wrong route/kind, missing fields |
| 401 | Missing or invalid bearer |
| 403 | Authenticated but not allowed (role / plan / paused) |
| 404 | Unknown id (or hidden cross-tenant id) |
| 409 | Conflict (duplicate, already in that state) |
| 410 | Retired endpoint (`POST /api/public/unsubscribe`) |
| 413 | Body larger than the message cap (~48 MiB) |
| 422 | Content gate hard-block or compliance gate |
| 429 | Rate limit or monthly cap |

## Common codes

| Code | What to do |
| --- | --- |
| `content_blocked` | Read `content_reasons`; use `POST …/send/score` |
| `suppressed` | Recipient is on a team/project/route list |
| `paused` | Reputation or abuse pause — fix the list, then resume |
| `plan_cap` | Monthly volume reached |
| `warm_up` | Wait or stay on transactional |
| `impressum_incomplete` | Fill street, city, privacy URL |
| `dkim_unverified` | Publish DKIM and re-check DNS |
| `route_kind_mismatch` | Newsletter → `broadcast`; termin → `outgoing` |
| `safe_mode` | Recipient not on the allowlist / simulator |
| `attachment_rejected` | Type, size, or double-extension |

Exact strings can grow; treat unknown codes as generic failures and show `hint`.

## Idempotency

`Idempotency-Key` (or `X-Idempotency-Key`) on single send, and `idempotency_key` on batch items / bulk, replay the **original** status and body for the same workspace + key. Keys are scoped per tenant.

## Rate limits

Werkmail applies a per-tenant client limiter in addition to the outbound platform. Bursting thousands of un-idempotent POSTs will 429. Prefer batch/bulk and backoff on 429.

HTTP bodies are capped at `MaxMessageRequestBytes` (tens of MiB) so a single request cannot exhaust the process.
