Retries and duplicates
What happens when your endpoint fails, and how to handle repeats.
Retries
A delivery succeeds when your endpoint answers with any 2xx status within
10 seconds. Anything else — a 4xx/5xx, a timeout, a connection error —
is retried with exponential backoff:
| Attempt | Roughly after |
|---|---|
| 1 | immediately |
| 2 | 1 minute |
| 3 | 4 minutes |
| 4 | 16 minutes |
| 5 | 1 hour |
| 6 | 4 hours |
| 7–8 | ~10 hours each |
That is 8 attempts over about 24 hours. Redirects are not followed — point the endpoint at the final URL.
Acknowledge first, work later
Return 200 as soon as the signature checks out and do slow work (calling
other APIs, sending emails) in a background job. Slow handlers hit the
10-second timeout and get retried.
Duplicates: at-least-once delivery
You may receive the same event more than once. For example: your server
processes an event, but its 200 reply is lost on the network. Zaher never
got the acknowledgement, so it retries.
Retrying is the safe choice — never retrying would lose events — so make
your handler idempotent (running it twice has the same effect as once).
The event id is identical on every retry, so record the ids you've
processed and skip repeats:
export async function handleZaherEvent(event: { id: string; type: string }) {
// A unique constraint on processed_events.id makes this race-safe.
const isNew = await db.processedEvents.insertIfAbsent({ id: event.id });
if (!isNew) return; // already handled
await process(event);
}Ordering
Events are not guaranteed to arrive in order — a retry of an older event
can arrive after a newer one. Use created_at to decide which is newest, and
remember data is the resource as it was when that event happened.
Failing endpoints are disabled
If an endpoint keeps failing for 3 days or 50 attempts in a row, Zaher disables it and notifies the store by email and in the dashboard. Fix the endpoint, then re-enable it from Settings → Webhooks. Re-enabling resets the failure counters.