Platform

What is idempotency, and how do I handle duplicate webhooks?

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/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 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 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 didWhat you get
Same key, same request, first attempt completedThe cached response, with Idempotency-Replay: true
Same key completed, then a different request body or endpoint409, E01005 IdempotencyKeyReuse
Same key, first attempt still running409, E01004 RequestInProgress
A key longer than 255 characters400, 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.

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 has the full failure-mode table and per-language examples, and Webhooks covers the delivery guarantees this relies on.

Bouw op hetzelfde netwerk.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra u een betaalmethode toevoegt en een afzender verifieert.

Begin met één kanaal.
Voeg de rest toe wanneer je er klaar voor bent.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra je een betaalmethode toevoegt en een afzender verifieert.

Gebruik je Claude Code, Cursor of Codex? Kopieer een setup-prompt en je agent installeert de Bird CLI en skills voor je. Kies de jouwe:

Cursor