---
title: "Errors"
description: "One shape, two levels, and a request id on everything."
url: "https://openemail.uk/docs/api/errors"
area: "API"
category: "Getting started"
---

# Errors

One shape, two levels, and a request id on everything.

## The envelope

`type` is a frozen set you can branch on and will never grow. `code` is specific and additive, so treat one you do not recognise as its `type`. `requestId` is on every response, successes included, and is what ties a report to a log line.

**422 Unprocessable Entity**

```
{
  "error": {
    "type": "validation_error",
    "code": "invalid_email_address",
    "message": "Not a valid email address: ada@",
    "param": "to.0",
    "docUrl": "https://openemail.uk/docs/api/errors#invalid_email_address",
    "requestId": "req_e103790543af4…"
  }
}
```

`param` is dotted and indexed, so it points at the exact element (`to.0`, `attachments.2.filename`) rather than at the field that contains it.

## Status codes

| Status | type | Common codes |
| --- | --- | --- |
| 400 | invalid_request_error | malformed_json, invalid_idempotency_key |
| 401 | authentication_error | missing_api_key, invalid_api_key, revoked_api_key, invalid_credential_type |
| 403 | permission_error | insufficient_scope, from_address_forbidden |
| 404 | not_found_error | resource_not_found |
| 409 | conflict_error | email_not_cancellable, translation_not_configured |
| 422 | validation_error | invalid_email_address, reserved_header, too_many_recipients, unknown_parameter, idempotency_key_reuse, label_not_directly_settable, unknown_language, translation_too_long |
| 429 | rate_limit_error | send_quota_exceeded, too_many_inboxes |
| 500 | api_error | internal_error |
| 503 | api_error | translation_failed |

> A 404 never distinguishes "does not exist" from "belongs to another workspace". That is deliberate: the difference is itself information.

> A 429 never carries `Retry-After`, so choose your own wait. `send_quota_exceeded` is the monthly send allowance and it resets on the first of the month, so show it to a person rather than backing off. `too_many_inboxes` is the disposable inbox mint ceiling, and extending an inbox you already hold costs nothing against it.

> 500 and 503 share a `type` and mean different things to a caller. A 503 is a dependency that did not answer (today that is the translator), and the request is worth retrying unchanged; a 500 is ours and is worth reporting with its `requestId`.

## Handling them

Branch on `type` for behaviour and read `code` for the message you show a human. An unrecognised `code` is not an error in your client. It means we named a failure more precisely than we used to.

**TypeScript**

```
const res = await fetch(`${BASE}/emails`, { method: 'POST', headers, body });

if (!res.ok) {
  const { error } = await res.json();

  switch (error.type) {
    case 'rate_limit_error':
      throw new RateLimited(error.code);
    case 'validation_error':
      // error.param points at the offending field
      throw new BadRequest(`${error.param}: ${error.message}`);
    case 'authentication_error':
      // revoked_api_key and expired_api_key are worth telling an operator apart
      throw new AuthFailed(error.code);
    default:
      // quote requestId when you report it
      throw new Unexpected(error.message, error.requestId);
  }
}
```
