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-1d0a2b3c4d5e

How it behaves

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

OutcomeRetry?
Connection timeout, no responseYes — with the same key
500, 502, 503Yes — with the same key, after a backoff
429Yes, after Retry-After
422 validationNo. It will say the same thing forever. Fix the request.
401, 403No. 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.