An idempotency key is a unique value attached to an API request so the server can recognize a retry and avoid performing the same operation more than once. It is one of the most important reliability patterns for APIs that create real-world side effects, including payments, orders, account changes, webhook processing, and transactional email sends.
The short definition of an idempotency key
An idempotency key is a client-generated identifier that represents one intended action. If the client sends the same request again with the same key, the API should treat it as a replay of the original request rather than a new action.
For example, suppose an application calls an endpoint to send a password-reset email:
POST /v1/emails HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: 8b560cd1-11d0-4c2c-9ea7-ea21d976a8bb
{
"from": "Security <security@example.com>",
"to": ["alex@example.net"],
"subject": "Reset your password",
"html": "<p>Use this link to reset your password.</p>"
}
If the client times out before receiving a response, it cannot know whether the server received the request. Retrying with a new idempotency key risks sending a second reset email. Retrying the same request with the same Idempotency-Key gives the API a way to say: “I already accepted and processed this action; return the original outcome instead.”
The key does not magically make every distributed-system problem disappear. It gives the client and server a shared identifier for one logical operation, allowing the server to suppress duplicate execution within a defined scope and retention period.
Why APIs need idempotency keys
Networks are unreliable in ways that matter to application code. A request can reach the server while the response is lost. A load balancer can close an idle connection. A mobile app can switch from Wi-Fi to cellular. A worker can crash after submitting work but before writing its success state to a database.
From the client’s perspective, these failures often look identical: a timeout, connection reset, or unavailable response. From the server’s perspective, the action may already have succeeded.
That uncertainty creates the classic question behind idempotency:
Did the request fail, or did only the response fail?
Without an idempotency mechanism, a retry can duplicate a side effect. Depending on the endpoint, that might mean:
- Charging a customer twice.
- Creating two orders.
- Provisioning two accounts or cloud resources.
- Applying an account credit twice.
- Sending duplicate receipts, verification messages, or password-reset emails.
- Recording the same webhook event multiple times.
- Triggering the same downstream workflow repeatedly.
A well-designed client generally should retry transient failures. But retrying a non-idempotent operation blindly is dangerous. The idempotency key is what lets a client retry safely.
HTTP method idempotency is not enough
HTTP itself defines some methods as idempotent. In broad terms, repeating an identical PUT, DELETE, GET, HEAD, or OPTIONS request is intended to leave the server in the same final state as sending it once. A DELETE /users/123 request might return 204 No Content the first time and 404 Not Found on a later attempt, but the desired state—user 123 no longer exists—remains the same.
POST, however, usually means “perform this action” or “create a new resource.” Two identical POST /orders calls might create two orders. Two POST /emails calls might create two messages. That is why idempotency keys are most commonly used with POST and sometimes PATCH.
The important distinction is this: HTTP method semantics describe a method’s intended general behavior. An idempotency key adds a way to make one particular client operation safely repeatable when the endpoint would otherwise create a new side effect each time.
How an idempotency key works step by step
The basic flow is straightforward, but reliable implementations need careful handling.
- The client creates a high-entropy key for one logical action, often a UUID.
- The client includes that key in the request, commonly in the
Idempotency-KeyHTTP header. - The server receives the request and checks whether it has already seen that key within the correct scope.
- If the key is new, the server records it, processes the operation, and stores enough information to resolve future retries.
- If the key is already complete, the server does not run the action again. It returns the saved response or a representation of the original result.
- If the key is already in progress, the server either waits, returns a conflict-like response, or tells the client to retry later—depending on the API contract.
Here is a simplified example of what the server might store:
| Field | Example value | Purpose |
|---|---|---|
| tenant or account ID | acct_7f3a... | Prevents one customer’s key from colliding with another’s |
| endpoint or operation | POST /v1/emails | Keeps unrelated operations separate |
| idempotency key | 8b560cd1-11d0-4c2c-9ea7-ea21d976a8bb | Identifies the logical client action |
| request fingerprint | SHA-256 of canonical request data | Detects reuse with different payloads |
| state | processing or completed | Coordinates concurrent retries |
| status code | 202 | Replays the original result consistently |
| response body | { "id": "msg_..." } | Lets the API return the original outcome |
| expiry time | timestamp | Defines how long duplicate protection lasts |
The implementation details vary, but the central rule should not: the same key must refer to the same intended operation.
What makes a good idempotency key
A good idempotency key must be unique enough that accidental collisions are extraordinarily unlikely. It should also be opaque: the server should not need to infer business meaning from it.
A UUID version 4 is a common practical choice:
8b560cd1-11d0-4c2c-9ea7-ea21d976a8bb
A UUIDv7, ULID, or securely generated random token can also be appropriate. The important properties are sufficient entropy, stable reuse for retries, and safe generation in the client environment.
Generate one key per intended action
Create the key when the user or system begins a logical operation—not immediately before every network attempt.
For example, if a user clicks “Place order,” generate the key once. Store it in the application state associated with that checkout submission. If the request times out, retry using that same key. If the user later makes a genuinely new purchase, generate a different key.
This is wrong:
async function createOrder(payload) {
return fetch("/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify(payload)
});
}
If retry logic calls createOrder() again, this code generates a new key and defeats duplicate protection.
This is better:
const idempotencyKey = crypto.randomUUID();
async function submitOrder(payload) {
return fetch("/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey
},
body: JSON.stringify(payload)
});
}
In production, keep the key with the job, transaction, message intent, or UI submission record so that retries made after process restarts still use the original value.
Do not use predictable values
Avoid using a customer ID, email address, current timestamp alone, or a sequential database integer as the whole key. Predictable keys increase collision risk and can create security and privacy problems if they leak into logs, URLs, or monitoring systems.
For example, this is poor:
alex@example.net-2026-08-14
It exposes personal data, may collide when multiple actions happen on the same day, and can be guessed. Use a random identifier instead, and keep business identifiers in authenticated request data or server-side records.
Scope the key correctly
A globally unique key is convenient, but server-side matching should still be scoped. At minimum, an API will commonly treat the tuple below as the idempotency identity:
authenticated account + HTTP method + route + idempotency key
That prevents the same literal key used by two separate customers from being interpreted as the same operation. It also prevents a key used for POST /v1/emails from accidentally colliding with POST /v1/orders.
Idempotency keys and transactional email
Transactional email is a useful example because duplicate messages are often operationally harmless but highly visible and frustrating for recipients. A duplicate password-reset message may confuse a user. A duplicate order confirmation can create support tickets. A duplicate invoice or account alert can undermine trust.
An email API has multiple stages:
- Your application creates a request to send a message.
- The email provider accepts the request.
- The provider queues and processes the message.
- The provider submits it to recipient infrastructure using SMTP.
- The receiving system accepts, defers, rejects, filters, or later bounces the message.
An idempotency key generally protects the first boundary: your application’s submission of one message intent to the provider. It does not guarantee that a recipient inbox displays exactly one message under every conceivable SMTP failure condition.
API acceptance is different from inbox delivery
Suppose an email API responds with 202 Accepted. That typically means the provider accepted the request for asynchronous processing; it does not mean Gmail, Outlook, Yahoo, or another destination mailbox provider has accepted the email.
Similarly, an SMTP relay may reply 250 after the DATA command, meaning the relay accepted the message for further handling. It is not necessarily a final inbox-delivery confirmation.
A simplified SMTP submission exchange might look like this:
S: 220 smtp.example.net ESMTP ready
C: EHLO app.example.com
S: 250-smtp.example.net
S: 250 STARTTLS
C: MAIL FROM:<receipts@example.com>
S: 250 2.1.0 Sender OK
C: RCPT TO:<alex@example.net>
S: 250 2.1.5 Recipient OK
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: Receipts <receipts@example.com>
C: To: alex@example.net
C: Subject: Your receipt
C:
C: Thank you for your order.
C: .
S: 250 2.0.0 Message accepted for delivery
If the application loses its connection after transmitting the final . but before receiving the relay’s 250 reply, it is in the same ambiguous state as an HTTP client that times out after sending a POST: the relay may have accepted the message even though the application does not know it.
SMTP itself has no universal Idempotency-Key command or standard header that every relay must use to deduplicate submissions. If you submit directly through SMTP, duplicate suppression is usually something your application must solve before calling the relay. For API-based sending, use an idempotency header or field when the provider supports one. For provider-specific details, check the relevant email API reference and setup documentation.
Message-ID is not a substitute
Developers sometimes assume the RFC 5322 Message-ID header solves duplicate prevention. It does not reliably do that.
A message ID identifies a message for threading and reference purposes, but receiving systems are not required to treat repeated submissions with the same Message-ID as duplicates. Some mailbox clients may display or thread them in ways that make duplication less obvious; others may show multiple copies. The sending system should not depend on Message-ID for exactly-once submission semantics.
Use an application-level idempotency key to stop duplicate creation at the API boundary. Set a valid Message-ID where appropriate for mail standards and threading, but treat it as a separate concern.
HTTP status codes and retry decisions
Idempotency keys make retries safer, not automatic. Your client still needs a deliberate retry policy based on the type of failure, the operation’s value, rate limits, and the provider’s documented behavior.
Responses that often justify a retry
The following outcomes can indicate a temporary condition, though an API’s own documentation always takes precedence:
- Connection timeout or TCP reset: The request may or may not have reached the server. Retry with the same idempotency key.
408 Request Timeout: The server did not receive a complete request in time. A retry may be appropriate.429 Too Many Requests: RespectRetry-Afterif present, apply backoff, and reuse the same key for the same operation.500 Internal Server Error: Potentially retryable, but preserve the key because the server may have completed work before failing to respond.502 Bad Gateway,503 Service Unavailable, or504 Gateway Timeout: Usually candidates for bounded retry with exponential backoff and jitter.
For SMTP, a reply code beginning with 4 is a transient negative completion reply. Examples include 421 for a service not available or closing transmission channel, and 451 for a requested action aborted due to a local processing error. A reply beginning with 5, such as 550, is generally a permanent failure for that attempt and should not be blindly retried without fixing the underlying problem.
Responses that usually should not be retried unchanged
A 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, or 422 Unprocessable Content normally means the request itself, credentials, permissions, route, or data needs correction. Retrying the same malformed request repeatedly creates load without solving the problem.
A 409 Conflict deserves special attention. Some APIs use it to mean that an idempotency key is already in progress, that the same key was reused with a different payload, or that the requested business action conflicts with current state. The response body should explain which case applies. Do not assume every 409 means the same thing.
A useful client policy often looks like this:
1. Generate and persist one key for the intended operation.
2. Attempt the request.
3. On an ambiguous network or transient server failure, retry with backoff.
4. Send the exact same idempotency key on every retry.
5. Stop after a bounded number of attempts or a time budget.
6. Query the resulting resource or reconcile asynchronously if the final outcome remains unknown.
Backoff and jitter matter
If thousands of workers retry at exactly 1, 2, 4, and 8 seconds after an outage, they can create a synchronized retry storm that makes recovery slower. Add random jitter to spread requests over time.
For example, rather than sleeping exactly four seconds, choose a random delay between zero and four seconds for a full-jitter strategy. Use a maximum attempt count or deadline so a failed dependency does not turn into an infinite queue of retries.
The critical rule: same key, same request
An idempotency key is not a reusable session token. It must be attached to one specific intended operation and reused only for retries of that operation.
If a client sends the same key with different request content, the server should not silently guess which payload is correct. Doing so can result in an old email body being delivered to a new recipient, the wrong amount being charged, or a confusing mismatch between a client’s request and its returned result.
Consider these two requests:
POST /v1/emails
Idempotency-Key: 8b560cd1-11d0-4c2c-9ea7-ea21d976a8bb
{"to":["alex@example.net"],"subject":"Verify your account"}
POST /v1/emails
Idempotency-Key: 8b560cd1-11d0-4c2c-9ea7-ea21d976a8bb
{"to":["sam@example.net"],"subject":"Verify your account"}
These are not retries of the same logical action. A robust API should reject the second request, commonly with 409 Conflict or 422 Unprocessable Content, and explain that the idempotency key has already been associated with a different request.
Request fingerprints
Servers commonly compare the incoming request against a stored fingerprint. A fingerprint might include:
- The authenticated account or API key identity.
- The HTTP method and normalized endpoint path.
- A canonicalized JSON payload.
- Relevant headers that change the operation’s meaning.
- A hash of attachment contents or stable attachment references, where applicable.
Be careful with canonicalization. JSON object key ordering is not meaningful, so these payloads should normally be considered equivalent:
{"to":"alex@example.net","subject":"Welcome"}
{"subject":"Welcome","to":"alex@example.net"}
But whitespace, omitted defaults, array order, timestamps, and generated values can complicate comparison. The simplest client rule is best: when retrying, send the same serialized request body and headers whenever possible.
Server-side implementation patterns
Correct idempotency implementation is more than checking a cache after the fact. The server must prevent two near-simultaneous requests with the same key from both performing the side effect.
The race condition to avoid
Imagine two retries arriving at almost the same moment:
- Request A checks storage and sees no record for key
K. - Request B checks storage and sees no record for key
K. - Request A sends an email.
- Request B sends the same email.
- Both create records afterward.
The existence check did not protect anything because it was not atomic.
A safer design uses a database uniqueness constraint or an atomic insert operation on the scoped key. The first request claims the key; later requests see the existing record and act according to its state.
A simple state model
A practical record state machine can have three states:
processing -> completed
processing -> failed
When the first request arrives, the service atomically creates a processing record. It then performs the operation.
If the operation completes, the service stores the result—such as the resource identifier, response status, and response body—and marks the record completed. A retry can then return the saved result rather than rerun the action.
If the operation definitely fails before any side effect occurs, the service can mark the record failed or delete it, depending on its contract. Be conservative: if the service cannot prove the side effect did not happen, preserve the record and reconcile it rather than allowing a duplicate execution.
In-progress duplicate requests
When a duplicate request arrives while the original is still working, APIs have several valid approaches:
- Wait for the original request to finish, then return its result.
- Return
409 Conflictwith a machine-readable error such asidempotency_key_in_progress. - Return
202 Acceptedwith an operation status URL. - Return
425 Too Earlyin designs where that status is explicitly documented and applicable.
The best choice depends on endpoint latency and architecture. Long-running email campaign creation, payment settlement, or file processing may be better modeled as asynchronous jobs. A quick resource creation operation may be able to wait briefly and replay the completed response.
Store results for a defined retention period
Idempotency records cannot be kept forever without cost. APIs commonly retain them for a stated window, such as hours or days, based on retry patterns and the business risk of duplicate execution.
The retention period is part of the contract. Once a record expires, reusing its old key may create a new action. Clients should therefore not rely on a stale key as a permanent deduplication mechanism.
For important business events, maintain a separate durable business-level uniqueness rule as well. For instance, an invoice system might enforce one receipt per order_id even after an API-level idempotency record has expired.
Idempotency is not exactly-once delivery
“Exactly once” is often used casually, but it is a stronger promise than most distributed systems can honestly provide end to end.
An idempotency key can often deliver at-most-once execution at a particular API boundary: the provider will not create the same accepted operation twice for the same key and matching request during its retention window. That is extremely valuable.
It does not necessarily prove exactly-once delivery to an end user, exactly-once processing by every asynchronous worker, or exactly-once consumption by downstream systems. Email delivery in particular includes multiple independent systems after your API request is accepted.
The outbox pattern complements idempotency
For an application that writes a database record and then sends an email, avoid a fragile sequence like this:
1. Save order in database.
2. Send confirmation email.
3. Crash before recording that the email was sent.
A restart may send a second confirmation because the application has an order but no reliable record of the prior send attempt.
The transactional outbox pattern helps. In the same database transaction that creates or updates business data, write an outbox event containing a stable event ID. A worker later reads that event and sends it with an idempotency key derived from the event ID, such as:
order-confirmation:order_01J7M4KX9X4R7M0Z
The worker records progress and retries safely. The email provider’s API-level idempotency key then becomes one layer in a broader system that handles crashes, database commits, retries, and duplicate queue deliveries.
Webhooks need the same mindset
Webhook providers generally deliver with at-least-once semantics. A destination endpoint can receive the same event more than once because it responded slowly, timed out, returned an error, or because the provider retried after an ambiguous result.
Your webhook handler should use the event’s stable provider-issued ID, or another documented unique event identifier, as its deduplication key. Store it before executing irreversible work. Return a successful 2xx response only once you have reliably accepted or processed the event according to your system’s design.
Do not use the webhook’s delivery attempt ID as the deduplication key if the provider assigns a new attempt ID on every retry. You want to deduplicate the underlying event, not each delivery attempt.
What idempotency does not solve in email infrastructure
Idempotency prevents duplicate submission intent. It does not replace email authentication, recipient validation, suppression management, content quality controls, or delivery monitoring.
For example, an idempotent request can still fail because the sender domain is not authenticated. A receiving mail system may reject or filter a message if SPF, DKIM, or DMARC are missing, misaligned, or otherwise fail policy checks.
A DMARC record is a DNS TXT record published at _dmarc plus the domain name. A basic monitoring record looks like this:
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"
A stricter policy can look like this:
_dmarc.example.com. IN TXT "v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:dmarc-reports@example.com"
Those DNS records influence how receiving systems evaluate messages claiming to be from example.com; they do not tell an API whether it has already accepted your POST /emails request. They solve a different layer of the problem.
Likewise, tools such as MXToolbox can help inspect DNS and mail configuration, while mail-tester.com can provide a practical deliverability-oriented assessment of a test message. They are useful operational tools, but neither replaces idempotency at the application boundary.
The distinction is worth keeping clear:
| Problem | Primary control |
|---|---|
| A client retries after a timeout | Idempotency key |
| A worker processes the same job twice | Durable job or event deduplication |
| A webhook is delivered repeatedly | Provider event ID deduplication |
| A message fails domain authentication | SPF, DKIM, and DMARC configuration |
| An address is invalid or risky | Address validation and bounce handling |
| A message is unwanted or poorly received | Consent, content quality, sending practices, and reputation management |
Practical implementation checklist
Whether you are building an API client or operating an API, use this checklist to make idempotency intentional rather than accidental.
If you are calling an API
- Generate one cryptographically strong key for one logical operation.
- Persist the key with the job, transaction, or UI submission before sending the request.
- Use the same key for every retry of that operation.
- Keep the method, route, body, and meaningful headers unchanged on retry.
- Retry only clearly transient or ambiguous failures, with exponential backoff and jitter.
- Honor
Retry-Afterwhen an API returns it. - Treat a response that says “same key, different payload” as an application bug, not a reason to generate another key automatically.
- Record the API’s returned resource ID so later workflows can reconcile state without guessing.
If you are designing an API
- Document whether idempotency is supported and which endpoints require or accept it.
- State the header or request field name exactly.
- Define required key format, maximum length, scope, and retention period.
- Atomically claim keys to prevent concurrent duplicate execution.
- Bind each key to a request fingerprint and reject incompatible reuse.
- Store enough response data to make replay behavior predictable.
- Explain behavior for in-progress keys, completed keys, failed keys, and expired keys.
- Emit safe logs and metrics for duplicate suppression without recording sensitive payload data unnecessarily.
- Test dropped responses, concurrent retries, process crashes, database failover, and queue redelivery.
A useful API error shape
An API can make client behavior easier by returning structured errors. For example:
{
"type": "https://api.example.com/problems/idempotency-key-reused",
"title": "Idempotency key is already associated with another request",
"status": 409,
"detail": "Reuse the original payload or create a new idempotency key for a new operation.",
"code": "idempotency_key_payload_mismatch"
}
Machine-readable codes let SDKs and applications react without parsing human language. This is particularly useful when the same API is consumed by background workers, browser clients, mobile apps, and third-party integrations.
Common idempotency key mistakes
The concept is simple enough that the most damaging failures usually come from small implementation shortcuts.
Generating a new key on every retry
This is the most common mistake. It turns every retry into a brand-new operation and provides no deduplication protection.
Reusing one key for multiple user actions
A key should not be a long-lived identifier attached to an entire browser session, checkout cart, customer account, or email recipient. Reusing it across distinct actions causes legitimate new operations to be mistaken for duplicates.
Ignoring payload mismatches
If a key is reused with a different payload, reject it clearly. Returning the previous result without warning can conceal client bugs. Executing the new payload can create an unexpected duplicate side effect.
Treating a 2xx response as final business success
A 200 OK, 201 Created, 202 Accepted, or SMTP 250 can be a meaningful success at one boundary, but your application should understand what that boundary is. An accepted email send request is not the same as inbox placement; a queued job is not the same as a completed business workflow.
Using idempotency as the only business rule
Idempotency records expire. They also operate at an API-boundary scope. Use durable domain rules where needed: one fulfillment per payment, one receipt per order, one welcome email per verified account, or one password-reset token per issuance policy.
Conclusion
An idempotency key is a unique identifier for one intended API action. Its job is to let clients retry safely when the network cannot tell them whether the original request succeeded.
For transactional email, that means preventing a timeout, worker restart, or retry loop from turning one intended message into multiple API submissions. For payments, orders, webhooks, and provisioning, it protects against the same class of duplicate side effects.
Use one high-entropy key per logical action, preserve it across retries, send the same request with that key, and expect the server to atomically record and replay the original result. Pair that API-level protection with durable application events, correct retry behavior, and sound email authentication and deliverability practices. That combination makes systems far more reliable when—not if—the network becomes uncertain.
FAQ
Is an idempotency key the same as a request ID?
No. A request ID usually identifies one HTTP attempt for logging and tracing. An idempotency key identifies one logical operation across one or more attempts. A retry may have a new request ID but should retain the same idempotency key.
Should I use an idempotency key for every POST request?
Use one for POST requests that create an important side effect and may be retried, such as sending email, charging a card, creating an order, provisioning a resource, or triggering a workflow. Read the API documentation because providers differ in which endpoints support the pattern.
How long should an API keep idempotency keys?
There is no universal duration. The retention window should cover realistic client and worker retry periods while balancing storage cost and business risk. Document it clearly; after expiry, the same key may no longer prevent a new operation.
Can I use the email Message-ID as an idempotency key?
No. Message-ID is an email message identifier used for standards and threading behavior, not a dependable API-level duplicate-suppression mechanism. Use a separate application-level idempotency key.
Does an idempotency key guarantee that only one email reaches the inbox?
No. It can prevent duplicate API acceptance for one send intent, but email delivery involves queues, SMTP relays, recipient servers, filtering, and mailbox behavior. Use idempotency for submission safety, then separately monitor delivery, bounces, authentication, and recipient engagement.