An idempotent operation is one you can perform twice and get the same result as performing it once. In a messaging API that matters in two directions at once, and the two use different mechanisms, so it is worth separating them before doing either.

**Outbound**, you send a request and the connection drops. Did the message go out? Idempotency lets you retry without risking a second send.

**Inbound**, Bird delivers a webhook and you receive the same event twice. Idempotency here is something your handler does, not something a header gives you.

## How do I retry a send safely?

Add an `Idempotency-Key` header. It is opt-in and applies to `POST`, `PATCH` and `DELETE`; a request without it is processed normally with no deduplication, and `GET` ignores it because it is already safe to repeat.

When Bird sees a key it has already completed, it returns the cached response, the same status and the same body, without re-running anything. You can tell a replay apart because it carries an extra header:

```http
HTTP/1.1 202 Accepted
Idempotency-Replay: true
```

Three properties of the key are worth knowing before you design one.

**It is yours to choose, up to 255 characters.** A deterministic key derived from your own entities is the better shape, something like `welcome-user/usr_abc123`, because retries after a process restart still share it. A random identifier per logical operation works too, as long as you keep it for every attempt of that operation rather than minting a fresh one per HTTP call.

**It expires.** A completed response is kept for three hours, which covers a normal retry schedule. A retry after that is processed as a fresh request, and no deduplication record survives.

**The [SDKs](/docs/sdks/concepts) already do it, and the CLI does not.** Every Bird SDK generates a key for each mutating request and reuses it across its own internal retries, so if you send through one you have safe retries without writing any of this. The CLI takes `--idempotency-key` as an explicit flag instead, which is the opposite default and worth knowing if you move a script from an SDK to the CLI.

If you submit [over SMTP](/docs/guides/email/smtp) rather than the API, the same mechanism is available as a message header, `X-Bird-Idempotency-Key`, so a retried submission does not queue a second copy.

## What happens if I reuse a key wrongly?

Bird distinguishes three cases rather than treating them alike, and only one of them is a replay.

| What you did                                                      | What you get                                                   |
| ----------------------------------------------------------------- | -------------------------------------------------------------- |
| Same key, same request, first attempt completed                   | The cached response, with `Idempotency-Replay: true`           |
| Same key **completed**, then a different request body or endpoint | `409`, [`E01005 IdempotencyKeyReuse`](/docs/api/errors/E01005) |
| Same key, first attempt still running                             | `409`, `E01004 RequestInProgress`                              |
| A key longer than 255 characters                                  | `400`, `E01002 InvalidRequest`                                 |

The second row is the one to design against. Reusing a completed key with a different request is treated as a bug in your key generation rather than a request to guess, so Bird refuses instead of handing back a response that does not match what you sent. The comparison covers the method, the endpoint, the path and query parameters and the raw body, so even a whitespace change counts as a different request. Do not retry that `409`; fix the key.

`RequestInProgress` is different and is worth retrying. It means a second request with the same key arrived while the first was still going, which usually means your client timeout is shorter than the request. The in-flight lock clears within thirty seconds, so wait briefly and try again.

One case deliberately does not cache: a `5xx` is never stored. A server error means Bird does not know whether your request took effect, and caching that would permanently block a successful retry. So retry a `5xx` or a timeout with the same key, and you get either the original success or a fresh attempt.

A failure does not consume the key either. A validation or business-rule rejection releases it, which is what lets you fix the payload and retry under the same key rather than having to invent a new one.

## Why do I get the same webhook twice?

Because delivery is at-least-once, by design. A delivery that fails is retried, a network hiccup can mean we sent it and never saw your acknowledgement, and a replay you trigger yourself sends it again on purpose. Any of those can put the same event in front of your handler more than once.

What makes it tractable is that a redelivery is identifiable. Every retry of a delivery carries the same `webhook-id`, so two arrivals of one event are recognisably one event rather than two.

## How do I make my handler idempotent?

Store the `webhook-id` and check it before you act.

Treat the id as a unique key in whatever store you already have, and let the duplicate insert be the signal rather than checking first. A check followed by an insert reopens the race you are trying to close, because two copies of the same delivery can both pass the check before either writes.

Your endpoint has five seconds to respond, though, which rules out doing the real work inside that transaction. So the shape that survives both constraints has two stages:

1. In one transaction, insert the `webhook-id` and enqueue a job. The deduplication and the enqueue commit together, so you never end up with the id recorded and no work queued, or the reverse.
2. Return `2xx` immediately. The worker does the real work, and is responsible for its own idempotence, because a job can be retried for reasons that have nothing to do with the webhook.

A duplicate delivery then fails at step 1 on the unique constraint, which is your cue to return `2xx` without enqueueing anything.

The same key covers a replay you trigger yourself, because a redelivered event carries its original `webhook-id`. So the deduplication you build for retries protects replays for free.

Events are also not ordered, so sort by the `timestamp` in the payload rather than by arrival. What that means for a handler that accumulates state, and the worked case where getting it wrong undoes a charge, is in [how failed webhooks are retried](/explained/platform/how-are-failed-webhooks-retried).

## What should I not rely on?

That deduplication cannot fail. Bird's own guidance is explicit: deduplication protects against duplicate side effects and does not control throughput, and if the store is briefly unavailable requests proceed without it. So design keys and handlers so that a rare duplicate is survivable rather than assuming the mechanism makes one impossible.

The same reasoning applies inbound. Deduplicating on `webhook-id` handles redelivery of the same delivery; it does not make an operation safe that would be wrong to perform twice for any other reason.

[Idempotency](/docs/guides/idempotency) has the full failure-mode table and per-language examples, and [Webhooks](/docs/guides/webhooks#delivery-semantics) covers the delivery guarantees this relies on.