Skip to content

Guides

Idempotency

A retried submission must never send twice. The Idempotency-Key header is how SMSend guarantees that.

The key is required, not optional

The header

Choose a value that is unique to the submission, and derive it from something in your own system rather than generating it randomly. An order id works; a fresh UUID on every attempt defeats the whole mechanism, because the retry then looks like a new request.

Idempotency-Key: order-1042
Maximum key length
255
Replay window
24 h
Stale in-progress claim taken over after
300 s
Retry-After on an in-flight conflict
5 s
Uniqueness is scoped to
api_key_id + key

The three outcomes

What happens on a repeat depends on whether the first request finished, and on whether the body is identical.

IN_PROGRESS                       →  409 REQUEST_IN_PROGRESS + Retry-After
COMPLETED, same fingerprint       →  the stored response, replayed byte for byte
COMPLETED, different fingerprint  →  409 IDEMPOTENCY_KEY_REUSED

Replaying a completed request

Send the same key with the same body inside the 24-hour window and you get the original response back — the stored bytes, not a freshly computed answer — along with a header telling you it was a replay. Nothing is sent a second time and nothing is billed again.

HTTP/1.1 202 Accepted
Idempotent-Replay: true
X-Request-Id: 01K3F7XQZ8V2N4M6P8R0T5CJWE

Reusing a key for a different request

The same key with a different body is a conflict, not a replay. This catches the bug where a key is derived from something insufficiently specific and two genuinely different batches collide.

409 Conflict
{
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED",
    "message": "This Idempotency-Key has already been used for a different request. Use a new key.",
    "retryable": false,
    "detail": { "idempotency_key": "order-1042" },
    "request_id": "01K3F7XQZ8V2N4M6P8R0T5CJWE"
  }
}

Retrying while the first attempt is still running

If the original request has not finished, the retry is refused with a retryable error and a Retry-After. Wait and try again — the claim is released as soon as the first attempt completes.

409 Conflict
{
  "error": {
    "code": "REQUEST_IN_PROGRESS",
    "message": "A request with this Idempotency-Key is already in progress. Retry shortly.",
    "retryable": true,
    "detail": { "idempotency_key": "order-2000" },
    "request_id": "01K3F7XQZ8V2N4M6P8R0T5CJWE"
  }
}

How the body is compared

The fingerprint is a hash of the method, the path, and the payload with object keys recursively sorted. Sorting the keys means a client whose JSON library serialises fields in a different order on the retry still produces the same fingerprint.

A refused submission releases the key

If a batch is refused — for insufficient funds, say — the idempotency record is deleted rather than kept. That means you can top up the wallet and retry the identical request with the identical key, which is exactly what a caller wants and what a naive implementation would prevent for 24 hours.