# Handle errors and limits

> Read the API's errors, retry the ones worth retrying without sending twice, page through lists and stay inside your rate limits.

Source: https://docs.soosh.io/guides/api/errors-and-limits/

Every request to `/v1` can fail, and some failures are worth trying again.
This guide shows how to tell them apart, how to retry safely, and which limits
your workspace has. The API reference has the rules in full: see its
[Introduction](https://docs.soosh.io/reference/api/).

## 1. Read the error

Every error has the same [shape](https://docs.soosh.io/reference/api/#errors), whatever the endpoint:

```json
{
  "error": {
    "code": "template_params",
    "message": "The template variables don't match the template.",
    "details": { "missing": ["2"], "unexpected": [] },
    "request_id": "req_8Yq2LhT4mWc9Rk1Zp6Ns"
  }
}
```

| Field | Always there | What it holds |
|---|---|---|
| `code` | Yes | What went wrong, from [the table below](#every-error-code). Act on this, not on `message`. |
| `message` | Yes | What went wrong, in plain English for you. It can change wording. |
| `field` | No | A JSON path to the field at fault, like `template.name`. Left out when no single field is at fault. |
| `details` | No | More about the error. Only the codes that list it in the table have it. |
| `request_id` | Yes | The request's ID, also in the `X-Request-ID` header of every response. Quote it when you contact support. |

The codes never change meaning. New ones may be added: treat a code you
don't know by its HTTP status.

## 2. Decide whether to retry

| Status | Codes | What to do |
|---|---|---|
| `429` | [`rate_limited`](https://docs.soosh.io/reference/api/errors/#rate_limited) | Wait the `Retry-After` seconds, then send it again. |
| `503` | `unavailable` | Wait the `Retry-After` seconds, then send it again. |
| `500` | `internal_error` | Send it again after a few seconds, with the same `Idempotency-Key`. If it keeps failing, contact support with the `request_id`. |
| `409` | `idempotency_in_progress` | The first request with this `Idempotency-Key` is still running. Wait a second or two and send it again. |
| Any other `4xx` | | Don't retry as it is. Fix the request, or the thing it names. |

Back off between tries, for example 1, 2, then 4 seconds, and give up after a
few. Retry only with an `Idempotency-Key` on a `POST`, so a retry never sends
a message twice.

## 3. Make retries safe with an Idempotency-Key

Send an [`Idempotency-Key`](https://docs.soosh.io/reference/api/#retrying-safely) header on any `POST`. Use a value that's unique to
the thing you're doing, like your own order ID: 1 to 255 printable ASCII
characters.

```sh
curl https://app.soosh.io/v1/messages \
  -H "Authorization: Bearer $SOOSH_API_KEY" \
  -H "Idempotency-Key: order-1042-shipped" \
  -H "Content-Type: application/json" \
  -d '{"to": "+919876543210", "type": "text", "text": {"body": "Your order has shipped."}}'
```

The key belongs to the API key that sent it, and to the keys you rotate it
into: a retry with the rotated key gets the same answer. The same request
under the same `Idempotency-Key` from any other of your API keys gets `422
idempotency_mismatch`. Soosh keeps it for 24 hours.

| You send the same key again… | You get |
|---|---|
| after the first request finished | The first response again, with `Idempotent-Replayed: true` and `Original-Request-ID` naming the first request. |
| while the first request is still running | `409 idempotency_in_progress` |
| after the first request crashed (60 seconds without finishing) | The request runs again. |
| with a different method, path or body, or from an API key that isn't this one or rotated from it | `422 idempotency_mismatch` |

Errors are kept too: a `422` [`window_closed`](https://docs.soosh.io/reference/api/errors/#window_closed)
stays `window_closed` for 24 hours under that key. A `5xx` isn't kept, so the same key can be retried. Send a
new key once you've fixed a request.

Use one on `POST /v1/messages`, `POST /v1/broadcasts` and
`POST /v1/contacts` at least.

## 4. Page through lists

A list returns at most `limit` items (1 to 100, 25 if you leave it out) and a
`next_cursor` (see [Pagination](https://docs.soosh.io/reference/api/#pagination)):

```json
{ "data": [ … ], "next_cursor": "eyJzIjoiY3JlYXRlZF9hdCIs…" }
```

To get the next page, send the same request with `cursor` set to
`next_cursor`. When `next_cursor` is `null`, you've reached the end. Lists
don't give a total count.

A cursor only works with the `sort` it was made for. A changed cursor, or one
used with another `sort`, is `400 invalid_request`. Lists that sync data
(contacts, messages and templates) take `sort=updated_at`, which runs oldest
change first, so you can keep the last cursor and carry on from it later.

A list by `sort=updated_at` shows a change about 30 seconds after it's saved,
so that changes still being saved can land first. That catches almost all of
them, not every one: when you carry on later, go back a minute before your
last cursor's time and skip items whose `updated_at` you already have. See
[Keep contacts in sync](https://docs.soosh.io/guides/api/sync-contacts/#by-asking-to-catch-up).

## 5. Stay inside your rate limits

| Limit | Counts | Usually |
|---|---|---|
| `api_requests_per_minute` | Every request from all of your workspace's keys together | 600 a minute |
| `api_messages_per_second` | `POST /v1/messages` | 20 a second |
| Heavy requests | `POST /v1/broadcasts`, `POST /v1/templates`, `POST /v1/media` and `GET /v1/usage` together | 10 a minute |

Your plan sets the first two. A plan with no limit on them is held at the
most Soosh allows: 60,000 requests a minute and 500 messages a second.
[`GET /v1/usage`](https://docs.soosh.io/reference/api/usage/get-usage/) shows the values your workspace has. Broadcasts don't count
against `api_messages_per_second`; they count against
`broadcast_messages_per_month`.

Each limit runs in fixed windows of a minute or a second. Every response to
a valid key carries the workspace limit (see
[Rate limits](https://docs.soosh.io/reference/api/#rate-limits)):

| Header | What it holds |
|---|---|
| `X-RateLimit-Limit` | Requests your workspace can make each minute. |
| `X-RateLimit-Remaining` | Requests left in this minute. |
| `X-RateLimit-Reset` | Seconds until the minute starts again. |

Over a limit you get `429 rate_limited` with `Retry-After`. To send many
messages at once, spread them out, or use a broadcast.

> **Careful:** After 50 requests with a wrong key from one IP address in 5 minutes, that
> address is locked out for the rest of the 5 minutes: only keys that worked in
> the last 24 hours still get in. IPv6 addresses are counted by their /64. A
> key used from an address it doesn't allow counts as a wrong key there. If
> your servers share an IP address, a new key, or one that hasn't been used for
> a day, waits until the lockout ends, so fix bad keys quickly.

## 6. Keep requests under the size limits

| Request | Largest body | Over it |
|---|---|---|
| Any JSON request | 1 MB | `400 invalid_request` |
| `POST /v1/broadcasts` | 4 MB | `400 invalid_request` |
| `POST /v1/media` | 5 MB for an image, 14 MB for other files | `413 too_large` |

Send bodies as they are: a compressed body (`Content-Encoding`) is
`415 unsupported_media_type`.

## 7. Watch messages after they're accepted

[`POST /v1/messages`](https://docs.soosh.io/reference/api/messages/send-message/) answers `201`
once the message is checked and saved, before WhatsApp has it. A `201` means
Soosh will send it, even if it stays `pending` for a minute: don't send it
again. What happens next shows in the `status` of the
[Message](https://docs.soosh.io/reference/api/messages/#object), in
[`GET /v1/messages/{id}`](https://docs.soosh.io/reference/api/messages/get-message/), and in the
[`message.sent`](https://docs.soosh.io/reference/api/webhook-events/message-sent/) and
[`message.status`](https://docs.soosh.io/reference/api/webhook-events/message-status/) webhooks:

- `pending` while it's on its way to WhatsApp.
- `sent`, `delivered` and `read` as it moves forward.
- `failed` with an `error`: WhatsApp's error `code` (0 if unknown) and a
  `message`. For example, code 131047 means more than 24 hours have passed
  since the customer last wrote, and you need a template.

If Soosh sent a message but WhatsApp didn't confirm it, the message stays
`pending` rather than being sent again. It settles when WhatsApp reports its
status, or fails after 15 minutes with "WhatsApp didn't confirm this message
in time". A message that couldn't be sent at all in 15 minutes fails with
"Not sent in time". See
[When a message fails later](https://docs.soosh.io/reference/api/errors/#when-a-message-fails-later).

## Every error code

The same codes, with the endpoints that return them, are in
[Errors](https://docs.soosh.io/reference/api/errors/#error-codes) in the API reference.

| Status | Code | When | `details` |
|---|---|---|---|
| `400` | `invalid_request` | The JSON or form can't be read, has a field the endpoint doesn't take or a wrong type, is over the size limit, or has a bad cursor or query value. | |
| `401` | `unauthorized` | The key is missing, malformed, unknown, revoked or expired. The response doesn't say which. | |
| `403` | `ip_not_allowed` | The key has allowed IP addresses, and the request came from another one. | |
| `403` | `scope_missing` | The key doesn't have the scope the endpoint needs. | `scope` |
| `403` | `feature_not_in_plan` | Your plan doesn't include the feature, such as `api.messages`. | `feature` |
| `403` | `workspace_closed` | The workspace is closed. | |
| `404` | `not_found` | No such path, or no such object. Objects of another workspace are `not_found` too. | |
| `405` | `method_not_allowed` | The path exists, but not with this method. The `Allow` header lists the methods it takes. | |
| `409` | `conflict` | It already exists, such as a contact with this number. | `id` of the one that exists |
| `409` | `invalid_state` | The object's state doesn't allow it, such as cancelling a completed broadcast, or deleting a template a scheduled or running broadcast uses. | |
| `409` | `idempotency_in_progress` | A request with this `Idempotency-Key` is still running. | |
| `413` | `too_large` | The file is over the size for its type. | |
| `415` | `unsupported_media_type` | The file isn't a type the API takes, or the body is compressed. | |
| `422` | `invalid_field` | A value failed a check. `field` says which. | `id` of the draft, only when WhatsApp refused a new template |
| `422` | `invalid_phone` | The phone number isn't valid. | |
| `422` | `idempotency_mismatch` | This `Idempotency-Key` was used with a different request. | |
| `422` | `window_closed` | A message other than a template, and the contact hasn't written to that number in the last 24 hours. | |
| `422` | `template_not_approved` | The template isn't approved. | |
| `422` | `template_params` | The template's variables don't match the ones you sent. | `missing`, `unexpected`, and for broadcasts `rows` |
| `422` | `template_ambiguous` | More than one of your numbers has a template with this name and language. Send `number_id`, or name the template by `template.id`. | |
| `422` | `contact_opted_out` | A marketing template to a contact who opted out of marketing messages. | |
| `422` | `number_unavailable` | The sending number isn't active. | |
| `422` | `limit_reached` | A limit of your plan is reached, such as contacts or broadcast messages this month. | `limit`, `limit_value`, `used` |
| `429` | `rate_limited` | Too many requests. Comes with `Retry-After`. | |
| `500` | `internal_error` | Something went wrong on our side. | |
| `503` | `unavailable` | Part of Soosh isn't available just now. Comes with `Retry-After`. | |

When a request has several problems, you get the first in this order: body
size, the key, the IP address, rate limits, scope, plan, closed workspace,
reading the body, the `Idempotency-Key`, then the checks on the values.

## Related

- [Introduction](https://docs.soosh.io/reference/api/)
- [Errors](https://docs.soosh.io/reference/api/errors/)
- [Get usage](https://docs.soosh.io/reference/api/usage/get-usage/)
- [Limits](https://docs.soosh.io/reference/limits/)
- [Verify webhooks](https://docs.soosh.io/guides/api/verify-webhooks/)
- [Make your first API call](https://docs.soosh.io/get-started/make-your-first-api-call/)
