Sending
Idempotency and retries
How to retry a request without sending twice, which statuses are safe to retry, and how long to wait.
A network can drop an answer after the server has done the work. If you retry a send blindly, the customer gets two messages. Two mechanisms prevent that; use both.
The Idempotency-Key header#
Send Idempotency-Key on every POST you might repeat. The key is 1 to 255 printable ASCII characters; a UUID, or an id of your
own job, is best.
curl -sS -X POST "$MYCOMM_URL/v1/messages" \
-H "Authorization: Bearer $MYCOMM_API_KEY" \
-H "Idempotency-Key: order-1094-ready-1" \
-H "Content-Type: application/json" \
-d '{"to": "+923001234567", "body": "Your order #1094 is ready.", "category": "transactional"}'
| What you do | What happens |
|---|---|
| First request with a key | It runs. A success, or a rejection caused by the request (a 4xx), is stored for 24 hours. |
| The same request again, same key | You get the stored answer with the header Idempotent-Replayed: true. Nothing runs twice. |
| The same key, a different request | 409 IDEMPOTENCY_KEY_REUSED. A key names one request. |
| The same key while the first request is still running | 409 IDEMPOTENCY_IN_PROGRESS. Wait a second and repeat. |
A request that failed with a 5xx |
Nothing is stored; repeating it with the same key runs it again. |
| A key that is empty, too long or not printable ASCII | 400 INVALID_HEADER, with details.reason. It is checked even on a dry run. |
Keys are scoped to your account: another account can use the same string. A dry run ("dry_run": true) never uses up its key.
Make the key part of your job record, not something you generate at send time: when the job is retried by your own queue, it must be the same key.
reference_id#
reference_id is your own id for a message, unique per account forever. If you send a reference_id that already exists
without a matching Idempotency-Key, the answer is 409 REFERENCE_ID_EXISTS and details.message holds the message that
already exists; nothing new is created. Treat that as "already sent". It protects you when the idempotency window (24 hours) has
passed, but it does not replace the key, because it cannot tell a retry from a mistake.
Which failures to retry#
| Outcome | Retry? |
|---|---|
| Connection error or timeout (you do not know what happened) | Yes, with the same Idempotency-Key |
5xx |
Yes, with the same key, with exponential backoff and jitter |
429 RATE_LIMITED |
Yes, after Retry-After seconds |
409 IDEMPOTENCY_IN_PROGRESS |
Yes, after a second |
503 SERVICE_UNAVAILABLE |
Yes, after a short wait, with the same key |
Any other 4xx |
No. The request is wrong or refused; repeating it unchanged gives the same answer. Fix it first. |
Back off: wait 1 s, 2 s, 4 s and so on up to about a minute, add random jitter, and stop after a limit you choose. Do not
retry a 422 in a loop: RECIPIENT_OPTED_OUT, INVALID_NUMBER and the like will not change by themselves.
Reads and other methods#
GET, PUT, PATCH and DELETE are safe to repeat: they have the same effect when repeated. PUT /v1/contacts/by-identity
is the idempotent way to keep contacts in sync. Only POST needs the header.
Retrying a message that failed#
That is a different thing: a message that was accepted and then failed. The platform retries the failures that are worth
retrying; for the others, POST /v1/messages/{id}/retry queues the same message again. See
the message lifecycle.