Skip to content

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.

10 minutesDevelopersUpdated

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.

Every error has the same shape, whatever the endpoint:

{
"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. 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.

Status Codes What to do
429 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

Section titled “3. Make retries safe with an Idempotency-Key”

Send an Idempotency-Key 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.

Terminal window
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 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.

A list returns at most limit items (1 to 100, 25 if you leave it out) and a next_cursor (see Pagination):

{ "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.

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 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):

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.

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

Section titled “7. Watch messages after they’re accepted”

POST /v1/messages 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, in GET /v1/messages/{id}, and in the message.sent and 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.

The same codes, with the endpoints that return them, are in Errors 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.