Network calls can fail after the server has already done the work, leaving you unsure whether to retry. Idempotency keys make a retry safe: send the same key and PristineSend guarantees the operation runs at most once, replaying the original result instead of sending again.
Idempotency is opt-in. Add an Idempotency-Key request header and the call becomes idempotent; omit it and behavior is unchanged. It applies to the two side-effecting send endpoints:
The read endpoints (Emails, Events, and so on) are naturally safe to retry and ignore the header. Creating a contact already has natural idempotency — a duplicate email returns 409 conflict — so it does not take a key today; key support there is a possible future extension.
You generate the key — a UUID v4 is a good default. It must be 1 to 255 characters. Keys are scoped per workspace and remembered for a 24-hour window; after that the key expires and reusing it starts a fresh operation.
| Header | Value |
|---|---|
Idempotency-Key | A unique string of 1–255 characters you choose per logical request (e.g. a UUID). Optional — present means idempotent. |
Use a new key for each distinct operation, and the same key for every retry of that operation. Reusing a key for genuinely different content is an error (see Errors).
When you retry with the same key and the same body, PristineSend returns the byte-identical stored response from the first call — same status code, same body. The replay carries an Idempotency-Replayed: true response header so you can tell a replay from a fresh execution.
A replay is a pure read of the stored outcome: it does not re-send the email, re-meter the send, or re-run any sending gate. That is the whole point — one logical request, one set of side effects, no matter how many times you retry.
For Batch send, the key covers the whole call — the entire batch is one idempotent unit. A retry with the same key replays the identical 207 response array (every item's result in order), and re-sends nothing. There are no per-item keys: you cannot make individual entries within a batch idempotent on their own. If you need per-item idempotency, send those items as separate /api/v1/send calls, each with its own key.
Three error codes are specific to idempotency. They use the standard error envelope (see Error codes):
| Status | code | When |
|---|---|---|
| 400 | idempotency_key_invalid | The Idempotency-Key header was sent but empty, or longer than 255 characters. |
| 409 | idempotency_key_in_progress | A concurrent request with the same key is still processing. Retry shortly. |
| 422 | idempotency_key_reused | The same key was sent with a different request body. The key is valid, but the params conflict. |
The distinction between 409 and 422 matters. idempotency_key_in_progress means the key is fine but a twin request is still running — back off and retry, and you will get the stored result. idempotency_key_reused means the key was already spent on a different body — pick a new key, because reusing one for new content is almost always a client bug. Example 422 body:
Only 2xx and 4xx outcomes are stored and replayed. A 5xx response — internal_error, provider_error, or a metering 503 service_unavailable — is treated as transient and releases the key. A retry through the same key then re-processes cleanly rather than replaying the failure. This is why you should keep retrying through the same key on a 5xx instead of minting a new one.
If a request claims a key and then the server dies mid-flight, the key does not stay locked until it expires. A claimed-but-unfinished key auto-recovers: a retry after about 90 seconds re-claims the key and processes the request normally. In the meantime, retries see 409 idempotency_key_in_progress — so a brief back-off and retry is the right response there too.
Generate one key per logical request and reuse it across every retry of that request:
See Error codes for the canonical error envelope and the complete list of codes.