# Errors

A request that fails gets a 4xx or 5xx status and one `error` object. Act on its `code`: codes never change meaning, though new ones may be added. Handle a code you don’t know by its status.

## The error object

- `code` (string, required): What went wrong, as one of the codes in the table below. One of `invalid_request`, `unauthorized`, `ip_not_allowed`, `scope_missing`, `feature_not_in_plan`, `workspace_closed`, `not_found`, `method_not_allowed`, `conflict`, `invalid_state`, `idempotency_in_progress`, `too_large`, `unsupported_media_type`, `invalid_field`, `invalid_phone`, `idempotency_mismatch`, `window_closed`, `template_not_approved`, `template_params`, `template_ambiguous`, `contact_opted_out`, `number_unavailable`, `limit_reached`, `rate_limited`, `internal_error`, `unavailable`.
- `message` (string, required): What went wrong, in plain English for a developer.
- `field` (string): A JSON path to the field at fault, for example `template.name`. Left out when no single field is at fault.
- `details` (object): More about the error, for the codes whose row below lists `details` keys.
- `request_id` (string, required): The `X-Request-ID` of the response.

## Error codes

| Code | Status | Meaning | details | Retry |
|---|---|---|---|---|
| `invalid_request` | 400 | The request is malformed: bad JSON or multipart, an unknown field, a wrong type, a bad cursor, an `Idempotency-Key` that isn't 1 to 255 printable ASCII characters, or a JSON body over 1 MB (4 MB for `POST /v1/broadcasts`). Fix the request. |  |  |
| `unauthorized` | 401 | The key is missing, malformed, unknown, revoked or expired. The reason isn't revealed. |  |  |
| `ip_not_allowed` | 403 | The client IP isn't in the key's allowed IPs. Call from an allowed address. |  |  |
| `scope_missing` | 403 | The key lacks the scope this endpoint needs. The detail names it. | scope |  |
| `feature_not_in_plan` | 403 | Your plan doesn't include the feature this endpoint needs. The detail names it. | feature |  |
| `workspace_closed` | 403 | The workspace is closed, so its API doesn't work. |  |  |
| `not_found` | 404 | There is no such path or object. It is also returned for an object in another workspace. |  |  |
| `method_not_allowed` | 405 | The path exists, but not for this method. The `Allow` header lists the methods it does accept. |  |  |
| `conflict` | 409 | The object is a duplicate. The detail is the ID of the existing object. | id |  |
| `invalid_state` | 409 | The object's state doesn't allow this, for example cancelling a completed broadcast. |  |  |
| `idempotency_in_progress` | 409 | A request with the same `Idempotency-Key` is still running. Wait, then retry with the same key. |  | Yes |
| `too_large` | 413 | A file is over the size for its type, an upload is over the upload limit, or the whole request is over 15 MB. A JSON body over its endpoint's limit is `invalid_request` instead. |  |  |
| `unsupported_media_type` | 415 | The file isn't a type the API accepts, or the request body is compressed. Request bodies can't have a `Content-Encoding`: send them as they are. |  |  |
| `invalid_field` | 422 | A field broke a rule. `field` names it. The detail is set only when WhatsApp refuses a template: see [Create a template](/reference/api/templates/create-template/). | id |  |
| `invalid_phone` | 422 | The phone number isn't valid. Check the number and send it again. |  |  |
| `idempotency_mismatch` | 422 | The `Idempotency-Key` was used before for a different request, or by another of your API keys. Use a new key. A key rotated from the first one counts as the same key. |  |  |
| `window_closed` | 422 | The contact hasn't written to this number in the last 24 hours. Send a template instead. See [The 24-hour window](/concepts/the-24-hour-window/). A retry with the same `Idempotency-Key` replays this error, so use a new key once the customer has written. |  |  |
| `template_not_approved` | 422 | The template isn't approved by WhatsApp yet. Use an approved template. |  |  |
| `template_params` | 422 | The template's variables are missing or extra, and the details say which. For a broadcast, `rows` lists the bad rows. | missing, unexpected, rows |  |
| `template_ambiguous` | 422 | Two of your numbers have a template with that name and language. Add `number_id`, or send the template's `id`. |  |  |
| `contact_opted_out` | 422 | The contact opted out of marketing messages. Don't send them a marketing template. |  |  |
| `number_unavailable` | 422 | The number isn't `active`. Use an active number. |  |  |
| `limit_reached` | 422 | You've reached a limit on your workspace. The details name it, give its value and say how much you've used. | limit, limit_value, used |  |
| `rate_limited` | 429 | Too many requests. Wait `Retry-After` seconds, then try again. |  | Yes |
| `internal_error` | 500 | Something went wrong on Soosh's side. Retry. For a `POST`, use the same `Idempotency-Key`. |  | Yes |
| `unavailable` | 503 | A service Soosh depends on is down. Try again after `Retry-After` seconds. |  | Yes |

## When a message fails later

A message that WhatsApp turns down after `POST /v1/messages` answered `201` comes as a `message.status` webhook with `status: failed` and an `error` holding WhatsApp’s own code.

A message still `pending` after 15 minutes fails the same way, with the `error.message` “Not sent in time” if it was never sent, or “WhatsApp didn’t confirm this message in time” if WhatsApp never said what became of it.