---
title: "What is an idempotency key, and when do you need one?"
description: "How idempotency keys prevent duplicate sends when a request is retried, how to choose one, and the failure modes they don't cover."
url: https://www.unitpost.com/blog/what-is-an-idempotency-key
section: Blog
updated: 2026-07-27
---
# What is an idempotency key, and when do you need one?

An idempotency key is a client-generated identifier you attach to a request so the server can recognise a retry of the same operation. Send it with an email and a repeated request within the dedupe window returns the original result instead of sending a second message. You need one anywhere a retry is possible: job queues, webhook handlers, and any agent-driven send.

## What is an idempotency key, and when do you need one?

> How idempotency keys prevent duplicate sends when a request is retried, how to choose one, and the failure modes they don't cover.

Networks fail in the worst possible way: the request succeeds but the response is lost. The client can't tell that from a request that never arrived, so it retries — and something happens twice that should have happened once.

### How the key fixes it

You generate a key identifying the operation and send it as a header. The server records the key with the result. A second request with the same key returns the stored result without performing the operation again. The retry becomes safe without the client needing to know whether the first attempt succeeded.

**cURL**

```bash
curl https://www.unitpost.com/api/v1/emails \
  -H "Authorization: Bearer pk_live_..." \
  -H "Idempotency-Key: order-confirmation-4821" \
  -H "Content-Type: application/json" \
  -d '{"from":"Acme <hi@acme.com>","to":"user@example.com","subject":"Order confirmed","html":"<p>Thanks!</p>"}'
```

**Node.js**

```ts
await unitpost.emails.send(
  { from, to, subject, html },
  { idempotencyKey: `order-confirmation-${orderId}` },
);
```

### Choosing a good key

The key must be stable across retries of the same logical operation and unique across different operations. That means deriving it from your domain model, not generating it fresh per attempt.

- Good: order-confirmation-4821 — tied to the thing being confirmed
- Good: password-reset-user-93-1721. — tied to the user and request
- Bad: a fresh UUID per attempt — every retry looks like a new operation
- Bad: password-reset — collides across users and across time

### The dedupe window

Keys don't live forever. A 24-hour window is the convention (Stripe and Resend both use it), which comfortably covers any realistic retry sequence while letting the same key be reused for a genuinely new operation later. A retry after the window sends a second message.

### Same key, different body

Reusing a key with a different payload is a bug, and the API says so rather than guessing: it returns 409. Silently sending the new message would violate the guarantee, and silently returning the old result would drop mail you intended to send. Failing loudly is the only safe answer.

### What idempotency doesn't solve

It protects against duplicate DELIVERY of the same operation. It does not deduplicate two genuinely different requests that happen to produce similar mail, and it doesn't help if your own code decides twice to send different messages. Those are application-level problems.

## FAQ

### Is an idempotency key the same as a message ID?

No. You generate the idempotency key before the request; the server generates the message ID after. The key identifies the operation you're attempting, the ID identifies the message that resulted. One is input, the other output.

### Do I need idempotency keys if I never retry?

You probably retry more than you think. HTTP clients retry on connection errors by default, job queues retry failed jobs, serverless platforms retry on timeout, and webhook senders deliver at-least-once. If any of those is in your path, retries happen whether you wrote them or not.

### What happens if I send the same key twice at the same time?

The second concurrent request is rejected rather than queued, because the first hasn't produced a result to return yet. Retrying shortly after gets the stored result. This matters for parallel workers that might pick up the same job.

## Related

- [Why domain verification should never be a one-time check](https://www.unitpost.com/blog/continuous-domain-verification): DNS changes after you verify a domain. How continuous re-verification with a grace period catches regressions without turning a transient blip into an outage.
- [How to let an AI agent send email safely](https://www.unitpost.com/blog/send-email-from-ai-agent): The concrete controls that make agent-initiated email safe: scoped credentials, a verified-domain gate, idempotency, approval before send, and an audit trail.
- [What is an email MCP server?](https://www.unitpost.com/blog/email-mcp-server): How Model Context Protocol exposes email sending as agent tools, what a well-designed email MCP surface looks like, and how authentication and scopes work.
