Idempotency
There is no unsend. A retry that duplicates a message is not an inconvenience — it is a text somebody received twice, and on an OTP it is two codes where one is now wrong.
Idempotency-Key: 4f2b9c1e-8a77-4f3e-9e51-1d0a2b3c4d5eHow it behaves
- Keys are unique per account and retained for 24 hours.
- A repeat with the same key returns the original message, unchanged. Nothing is sent a second time.
- The same key after 24 hours is a new request. If you are retrying a day later, you are not retrying — you are sending again.
Generate it at compose time, not at retry time
This is the whole trick, and it is easy to get backwards.
// Right — the key exists before anything is attempted.
const key = crypto.randomUUID();
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await send({ to, body, idempotencyKey: key });
} catch (e) {
if (!shouldRetry(e)) throw e;
await backoff(attempt);
}
}// Wrong — a fresh key per attempt is no key at all.
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await send({ to, body, idempotencyKey: crypto.randomUUID() });
} catch (e) { … }
}The failure case is a request that we received and processed, whose response you never saw. From your side that is indistinguishable from a request that never arrived. The key is what tells the two apart — and a key minted during the retry was not present on the attempt that actually landed.
This is also how our own mobile app works offline. A queued message gets its key when the user presses send, so a phone that reconnects and flushes a queue it is not sure about cannot text anyone twice.
What to retry, and what not to
| Outcome | Retry? |
|---|---|
| Connection timeout, no response | Yes — with the same key |
500, 502, 503 | Yes — with the same key, after a backoff |
429 | Yes, after Retry-After |
422 validation | No. It will say the same thing forever. Fix the request. |
401, 403 | No. Fix the credential. |
The same rule applies to us
When we submit your message to a carrier and the connection times out, we do not know whether they took it. We record it as needing reconciliation rather than as failed, and wedo not try another carrier — because a failover on a timeout is exactly how a platform delivers the same message twice. A background sweep then asks the carrier what actually happened.
We ask you to send an idempotency key for the same reason we refuse to failover on a timeout: an ambiguous outcome is not a failure, and treating it as one costs somebody a duplicate text.