Glossary

Idempotency key

An idempotency key is a caller-supplied string naming one intended message rather than one attempt at sending it, so a retried request replays the original response instead of producing a second email.

Definition checked against the implementation on .

An HTTP request times out. The agent does not know whether the server accepted it. Sending again may produce two emails to a customer; doing nothing may lose the message. The Idempotency-Key header removes the guess, but only if the key is derived correctly, because a carelessly chosen key reintroduces the exact duplicate it was supposed to prevent.

The rule in one sentence

The key names the message you meant to send, not the attempt you are making.

invoice-4471-receipt-v1 is a good key. It identifies one intended message and it survives a process restart, because it can be reconstructed from the invoice ID. A fresh crypto.randomUUID() generated inside the retry loop is the worst available key: it makes every attempt a distinct message and turns the header into decoration.

Write the intended payload and the key to durable storage before the first attempt. An in-memory value is not enough when a worker dies between issuing the request and recording the response, because on restart it has no way to learn which attempt was already in flight.

What the key is bound to

Not the raw request body. The request is normalised first into a canonical payload:

{ inbox_id, action, reply_to_message_id, to[], cc[], bcc[], subject, text, html, attachment_ids[] }

Recipient domains are lower-cased, local parts are left alone, and a missing text or html becomes an empty string. The object is serialised with sorted keys, so field order in your JSON is irrelevant, then hashed with SHA-256. The idempotency record is keyed on the tuple (workspace, project, "send", sha256("send:" + your key)) and stores that payload hash alongside the eventual response.

Two consequences follow directly. The record is per project, so the same key string in a test project and a live project are different records, which is what lets one deterministic key scheme run in both. And only the canonical fields count: changing metadata does not change the hash, while changing a recipient’s display name does, because the name is part of the recipient object.

The four outcomes of reusing a key

SituationResponse
Same key, same canonical payloadHTTP 202, the original response replayed
Same key, different payloadHTTP 409 idempotency_conflict
Same key, first attempt still in flightHTTP 503 temporarily_unavailable
Same key, more than 30 days laterHTTP 409 idempotency_expired

The replay window is 30 days. The key itself is then held as a tombstone for a further 365 days, so it cannot be silently recycled into a different message a year later.

The conflict case is the useful one. It fires exactly when your code believes it is retrying and is in fact sending something new, which makes it a bug detector rather than an obstacle. A client library that catches the 409 and retries with a fresh key has converted a caught bug into a duplicate email. Log it and stop. Retrying an agent’s email safely covers the key derivation patterns that avoid getting there.

What an idempotency key is not

It is not event deduplication. Inbound event delivery is at-least-once and dedupes on event ID, in your own storage, with your own record of what you have already processed. The two mechanisms protect different halves of the loop and neither substitutes for the other, which is the distinction handling webhooks without processing events twice exists to make.

It is not a lock. A 503 on a concurrent attempt means a matching send is in progress, not that your request has been queued behind it. Back off and re-read the message.

And it does not extend to approvals. Approving a held message requires its own Idempotency-Key, keyed in a separate namespace from the send it releases, so a double-clicked approve button is safe for a reason unrelated to the send key. The full request and response shapes are on the developer overview.

Read further

idempotencyreliabilityapi

All terms · Documentation · Guides