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

Idempotency makes a repeated operation have the same effect as one operation, so handle duplicate webhooks without repeating their work.

A dropped connection can leave you unsure whether a send request succeeded. A lost acknowledgment can also cause a webhook sender to deliver an event your application already stored.

These failures happen in opposite directions. Bird can recognize a repeated API request using a key you supply. Your webhook receiver needs its own record of events it has accepted.

How do I retry a send safely?

Reuse the same Idempotency-Key and identical request for each retry. The guarantee is exactly-once acceptance: matching retries within three hours must resolve to one accepted operation, even if you lost its response.

You choose the key, up to 255 characters, and keep it across retries. A stable value such as welcome-user/usr_abc123 can identify one welcome-message operation after your process restarts.

The header applies to mutating requests such as POST, PATCH and DELETE. A request without a key is processed without this deduplication. GET ignores the header because reading the resource is already safe to repeat.

Bird returns the stored response for a matching completed request, including its original status and body. The response carries Idempotency-Replay: true, so you can identify that reuse in your logs.

The idempotency guide documents a three-hour default for the completed-response window. A retry after it expires can run as a new operation. Do not rely on that key permanently to prevent duplicate sends.

Bird SDKs generate a key for a mutation and reuse it across their internal retries. The Bird CLI also generates a key for a mutating request when one is absent. Set --idempotency-key explicitly when separate command invocations must share the same operation.

For SMTP submission, use the X-Bird-Idempotency-Key message header. This lets a retried submission identify the same operation.

What happens if I reuse a key wrongly?

Bird rejects conflicting key use instead of returning a response for a different operation.

SituationResponse and recovery
Same key and request, retained responseThe stored response within retention, with Idempotency-Replay: true.
Completed key reused for a different request409 with E01005, meaning idempotency key reuse. Correct the key before retrying.
Another request with that key is still running409 with E01004, meaning request in progress. Wait briefly and retry.
Key exceeds 255 characters422 with E01001 on endpoints declaring the header. Shorten the key.

The comparison includes the method, endpoint, path parameters, query string and body. For JSON, changing whitespace changes the request identity, so preserve the original body during retries.

If the original request is still running, wait before retrying. Keep the same key and request so the retry identifies the original operation.

The API does not retain a 5xx response as a completed response. Retry a timeout or server error with bounded backoff, the same key, and an identical request within three hours. The exactly-once acceptance guarantee also covers a lost response.

A 4xx rejection can be retained. Use a new key for a corrected request: an unchanged retry replays a retained rejection, and changed input conflicts with that retained key.

Why do I get the same webhook twice?

Bird can retry an event your receiver already stored if it does not receive a successful response.

A receiver can store an event just before its connection drops. Bird sees no successful acknowledgment and retries, even though the receiver already has the event.

Each retry retains the same webhook-id header, which identifies the event. A replay of a failed delivery retains that identifier too, so both can be recognized as the same event.

How do I make my handler idempotent?

Store each webhook-id under a unique database constraint before scheduling the event's work.

Checking for an existing row before inserting leaves a race: two concurrent requests can both see no row. Let the database reject duplicate identifiers.

Store the identifier and the job in the same transaction. This prevents an identifier being recorded without any work queued.

  1. Verify the request, then insert its identifier and job in the same transaction.
  2. Return 2xx after that transaction commits, so Bird can stop retrying.
  3. Process the stored job in a worker that can safely repeat its own actions.

For a duplicate identifier already committed, return success without creating another job. For a failed transaction, return an error so Bird retries.

Keep slow work out of the receiver because waiting for it can cause the request to time out. A worker can retry for reasons unrelated to webhook delivery, so protecting only the receiver is insufficient.

Events can also arrive out of order. Compare event times in timestamp before overwriting newer state. Failed webhook retries includes the partial-cost example.

What should I not rely on?

Do not assume that request deduplication makes duplicate side effects impossible.

For a supported HTTP write, Bird ties the idempotency key to the request’s workspace or organization. Supported staff operations use the acting staff account instead. If protection is unavailable, Bird returns 503 E01033 IdempotencyUnavailable before executing that attempt. This does not resolve an earlier attempt’s outcome. The exactly-once acceptance guarantee covers matching retries within three hours. Check the message status to confirm delivery. If retries are exhausted or the window expires, check the original operation’s outcome before submitting again.

Likewise, webhook-id distinguishes repeated deliveries of one event. Separate events have separate identifiers. Your application still decides whether those events justify repeating the same action.

Idempotency documents API retry behavior. Webhooks covers the separate delivery guarantees your receiver handles.

In short

  1. API retries and webhook retries need different records.

    Reuse Idempotency-Key for a request to Bird. Your receiver stores webhook-id to recognize an event it already accepted.

  2. Rejections can also be retained.

    A retained rejection replays. A corrected request needs a new key because its input differs.

  3. A completed key cannot identify different requests.

    A changed JSON body or endpoint can produce a 409 conflict. Fix the key rather than retrying that conflict unchanged.

  4. Deduplication has limits.

    The API guarantee covers exactly-once acceptance of matching retries within three hours. Your webhook receiver needs its own durable event records.

Build on the same network.

A test API key is yours immediately. Production unlocks when you add a payment method and verify a sender.

Start building with Bird in minutes.