What an error looks like
A 4xx or 5xx status and an error object. code never changes for a given problem, so that's the thing to check. message is written for people and may be reworded. details carries specifics when there are any, and is null otherwise.
HTTP/1.1 402 Payment Required
{
"error": {
"code": "insufficient_credit",
"message": "This send needs 12 credits and the balance is 3.",
"details": { "needed": 12, "balance": 3 }
}
}Error codes
| Status | Code | What it means |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 401 | unauthorized | The API key is missing, mistyped or revoked. |
| 402 | insufficient_credit | Not enough credit for the whole send. details.needed and details.balance say by how much; nothing was sent. |
| 403 | account_pending | Your account has not been approved yet. Test keys still work. |
| 403 | account_suspended | The account is suspended. Email support. |
| 403 | test_mode_not_supported | Not possible with a test key: sending or cancelling a campaign, or cancelling a live text. |
| 404 | not_found | No such resource in your account. Another account's ids read as not found too. |
| 409 | idempotency_conflict | This Idempotency-Key was used before with a different body. Use a new key for a new request. |
| 409 | not_cancellable | The text or campaign has already gone (or finished), so it can no longer be cancelled. |
| 409 | not_editable | Only a draft campaign can be edited. |
| 409 | not_deletable | Only a draft campaign can be deleted. Cancel it instead. |
| 409 | already_launched | The campaign has already been sent or scheduled. |
| 409 | launching | The campaign is being sent right now. Try again in a moment. |
| 409 | contact_exists | Another contact already has that number. |
| 409 | sender_exists | You have already asked for that sender name. |
| 413 | body_too_large | The body is over 1 MB. |
| 422 | validation_failed | The request isn't valid. details lists the fields and what's wrong with each. |
| 422 | no_valid_recipients | Every recipient was refused. details.rejected gives the reason for each. |
| 422 | body_too_long | The text is longer than 10 SMS parts. |
| 422 | sender_not_approved | The sender name is not approved for your account. |
| 422 | invalid_url | A webhook URL is not a valid https URL, or contains a username or password. |
| 422 | unresolvable_host | A webhook URL's host could not be found. |
| 422 | private_address | A webhook URL points at a private or local network address. |
| 422 | too_many_webhooks | An account can have up to 10 webhook endpoints. |
| 429 | rate_limited | Too many requests. Wait for the number of seconds in the Retry-After header. |
| 500 | internal_error | Something went wrong on our side. Safe to retry with the same Idempotency-Key. |
Rejected numbers
A send with some bad numbers still goes to the good ones. The refused numbers come back in data.rejected, each with a reason, and none of them are charged. Only when every number is refused does the whole request fail, with no_valid_recipients.
| Reason | Why the number was refused |
|---|---|
invalid_number | Not a phone number we can read. |
unsupported_destination | A number outside the UK, Guernsey, Jersey and the Isle of Man. |
not_mobile | A UK landline or other non-mobile number. |
duplicate | The same number appeared more than once in the request; it gets one text. |
opted_out | The number is on your opt-out list. |
too_long | With merge fields filled in, the text for this person ran past 10 parts. |
empty_body | With merge fields filled in, the text for this person was empty. |
When to retry
Retry 429 after the Retry-After wait, and 500 or a dropped connection after a short pause. Send the same Idempotency-Key each time: if the first attempt did get through, you get its original answer back (with meta.replayed set to true) instead of a second send.
Don't retry other 4xx errors as they are; something in the request or the account needs changing first. The API reference lists which errors each endpoint can return.