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 header for every attempt of one logical API operation.
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.
| Situation | Response and recovery |
|---|---|
| Same key and request after completion | The stored response, with Idempotency-Replay: true. |
| Completed key reused for a different request | 409 with E01005, meaning idempotency key reuse. Correct the key before retrying. |
| Another request with that key is still running | 409 with E01004, meaning request in progress. Wait briefly and retry. |
| Key exceeds 255 characters | 400 with E01002, meaning invalid input. 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.
The lock on an unfinished operation expires within thirty seconds. That limit lets another request proceed after an abandoned operation. It does not establish whether a side effect already occurred.
Bird does not store a 5xx response for replay. Retry a server error or timeout with the same key so a recorded success can still be reused.
A validation or business-rule rejection releases the key. You can correct that rejected request and retry under the same key because no completed response was retained.
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 missed 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.
- Verify the request, then insert its identifier and job in the same transaction.
- Return
2xxafter that transaction commits, so Bird can stop retrying. - 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.
If Bird's deduplication store is unavailable, requests proceed without it. Keep a business-level safeguard where repeating an action would be harmful.
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
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.
Rejected requests can release their keys.
Validation and business-rule errors leave no completed response, allowing a corrected retry under the same key.
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.
Deduplication has limits.
Requests proceed if the deduplication store is unavailable. Keep repeated actions survivable in your application as well.