# Error codes

> All the API's error codes, what they mean, and whether retrying is worthwhile.

Every error has the shape `{ "error": { "code", "message", "details?", "requestId" } }`. **Base your code on `code`**, never on the text of `message`. Keep the `requestId`: it is what lets us find the call.

**Language of `message`**: French by default. Send `Accept-Language: en` to receive it in English; the `code` never changes. Texts returned by Meta (for example the reason a template was rejected) are not translated.

## Code table

| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
| `validation_error` | 400 | A field is invalid or missing; `details` lists it. | No, fix it first. |
| `unauthorized` | 401 | Key or session missing, invalid, expired or revoked. | No, check the token. |
| `quota_exceeded` | 402 | Your plan's monthly quota is reached (production). | No, change plan or wait for next month. |
| `plan_limit_reached` | 402 | The plan does not allow more WhatsApp numbers (tier cap), nor, on the default plan, more connected businesses. | No: change plan. |
| `forbidden` | 403 | The key lacks the required scope; the message names it. | No, create a key with the right scope. |
| `sandbox_only` | 403 | Action restricted to sandbox keys. | No. |
| `not_found` | 404 | The resource does not exist, **or belongs to another account or environment**. | No. |
| `conflict` | 409 | Duplicate (name, external reference, WhatsApp account already connected…) or action not possible in the current state (delivery in progress, endpoint disabled). | Depends on the case. |
| `invalid_message` | 422 | Invalid message: malformed number, empty text, ambiguous media, non-`https` link… | No. |
| `outside_service_window` | 422 | Free-form message outside the 24-hour window. | No: send a template, or wait for the customer to write. |
| `template_not_approved` | 422 | The template is not approved by Meta. | No, wait for approval. |
| `idempotency_conflict` | 422 | Same `Idempotency-Key` for a different request. | No: use another key. |
| `meta_error` | 502 | Meta rejected the request or is not responding; the message reflects its response. | **Yes** if it is an outage, no if it is a rejection. |
| `internal_error` | 500 | Error on our side. | **Yes**, then contact support with the `requestId`. |

## Real examples

### 400 — `validation_error`

<ErrorExample name="validation_error" />

`details` indicates the field and the rule that was violated.

### 401 — `unauthorized`

<ErrorExample name="unauthorized" />

### 403 — `forbidden`

<ErrorExample name="forbidden" />

### 404 — `not_found`

<ErrorExample name="not_found" />

<Callout type="note" title="Another account's resource also returns 404">
To avoid revealing the existence of resources that are not yours, a valid identifier that belongs to another account (or to the other environment) returns `404`, exactly like a nonexistent identifier.
</Callout>

### 409 — `conflict`

<ErrorExample name="conflict" />

### 422 — `outside_service_window`

<ErrorExample name="outside_service_window" />

### 422 — `invalid_message`

<ErrorExample name="invalid_message" />

### 422 — `idempotency_conflict`

<ErrorExample name="idempotency_conflict" />

## Message delivery errors

Once the message has been **accepted** (`202`), failures no longer appear in an HTTP response: the message moves to `failed`, with an `error` that carries Meta's code and message, and a `message.failed` event is sent to your webhooks.

| Situation | Automatic retry? |
|---|---|
| Meta outage, timeout, rate limit | Yes, up to 6 attempts with increasing backoff |
| Permanent rejection (template not found, recipient unreachable, content rejected) | No: `failed` immediately |

The numeric codes that appear in `error.code` are **Meta's**: see its documentation for their exact meaning.
