Idempotency & retries
Recover from interruptions without sending a duplicate.
A network timeout does not tell you whether the server accepted a request. Idempotency gives one logical operation a stable identity so you can retry it safely.
Where keys are required
| Operation | Header |
|---|---|
| Send from an inbox | idempotency-key |
| Reply to a thread | idempotency-key |
| Lower-level direct send | idempotency-key |
| Create a webhook | idempotency-key |
Use 8–200 characters matching ^[A-Za-z0-9._:-]+$. A prefix followed by a UUID is a good choice.
export SEND_KEY="send-$(uuidgen)"Create the key once per logical action and persist it with the request data. Do not regenerate it inside a retry loop.
Retry rules
- Retry ambiguous requests on the same route with the same key and semantic body.
- A different message or reply needs a fresh key.
- Reusing a key with changed content causes a conflict.
- A previously failed operation can replay its original failure; a retry does not automatically bypass the original policy decision.
- After
410, reconcile what was deleted. Do not blindly recreate the operation with a fresh key.
Use bounded backoff: wait 1, 2, 4, then at most 5 seconds between attempts. Honor Retry-After when present, even if it is longer than that normal schedule, provided the wait fits your deadline. Stop when the deadline expires.
Resources without a create key
For inbox creation, choose an explicit localPart. After an ambiguous response, match it plus domainKind: "managed" (or your exact customer domainId) across every inbox page, then save the returned address. See creation recovery.
For domain registration, list and match the normalized domain. Repeated registration of the same domain converges, but still retain the original domain ID.
For DELETE, the goal is confirmed absence. Retry pending deletion and verify absence using the owning read/list operation. Do not assume an interrupted DELETE failed.
A mailbox replay returns 410
error | Meaning | Recovery |
|---|---|---|
idempotency_result_expired | The original mailbox result was deleted; its key cannot be reused. | Stop automatic send/reply retries. If you saved submission.id, read that submission to reconcile delivery. |
mailbox_message_deleted | The stored message is being deleted or was deleted. | Stop replaying or replying to that message. Refresh its owning thread; do not select a replacement target automatically. |
{
"error": "idempotency_result_expired",
"message": "The original mailbox result was deleted; this idempotency key cannot be reused"
}Deletion of a mailbox record does not prove that an email was never delivered. If the submission is also unavailable or its ID was lost, retain the original key/input and report delivery as unknown; there is no idempotency-key lookup endpoint. A new key would create a new logical send and requires a deliberate application decision. The same rules apply to inbox sends and thread replies. Webhook registration uses its own conflict/expiry codes.