Webhook Retry & Idempotency
Allison makes up to six delivery attempts over ~31 hours. Every retry reuses the same event id so deduping is a one-line check.
What counts as success
A delivery is considered successful when your receiver returns a 2xx status within 10 seconds. Anything else is a failure:
- Any non-2xx status (including 3xx — we don't follow redirects)
- TCP connect failure, TLS handshake failure, DNS failure
- Timeout after 10 seconds
- Connection reset mid-response
Retry schedule
Retries are scheduled at increasing intervals from the initial attempt:
| Attempt | Delay from previous | Approx. elapsed |
|---|---|---|
| 1 | immediate | 0 |
| 2 | + 1 minute | ~1 min |
| 3 | + 15 minutes | ~16 min |
| 4 | + 1 hour | ~1h 16min |
| 5 | + 6 hours | ~7h 16min |
| 6 | + 24 hours | ~31h 16min |
After the sixth failure, delivery is marked permanently failed. You can still see the attempt in the dashboard (Settings → Webhooks → Deliveries) or via GET /v1/webhook-subscriptions/{id}/deliveries.
Idempotency: dedup on event.id
Every event has a stable id (evt_<32 hex>). Retries of the same event reuse the same id. Your receiver should track which event ids it has already processed and acknowledge duplicates immediately with 200.
// Pseudo-code for a minimal idempotent receiver
async function handle(req, res) {
const eventId = req.headers['x-allison-event-id'];
if (await seenEventIds.has(eventId)) {
return res.status(200).end(); // dedupe
}
// Verify signature (see /docs/webhooks/signing) BEFORE recording, so
// a forged request can't poison the dedup set.
if (!verify(req)) return res.status(401).end();
const event = JSON.parse(req.body);
await doWork(event);
await seenEventIds.add(eventId, { ttlDays: 7 });
return res.status(200).end();
}The event id is also in the envelope body (event.id), so you can read it either way. Keeping dedup entries for 7 days covers the full 32h retry window with plenty of margin.
Auto-disable
We automatically disable a subscription when it's clearly broken, so dead endpoints don't accumulate noise. Two triggers:
410 Goneresponse. Immediate disable. The receiver is signalling "this endpoint is retired, stop sending." Use this when you permanently remove a webhook handler.- Rolling failure rate ≥ 90% over 24 hours. Disabled once the subscription has seen at least 10 deliveries in the window and the failure rate crosses the threshold. Prevents a broken endpoint from exhausting retries forever.
Disabled subscriptions stay in the dashboard with an Auto-disabled badge and auto_disabled_reason field. Re-enabling via the dashboard or PUT /v1/webhook-subscriptions/{id} with { "enabled": true } clears the auto-disable state and resumes delivery on the next matching event.
Ordering
Delivery order is not guaranteed. Events for one call can arrive out of sequence even without retries (for example, call.recording_ready often lands before call.ended). If you need ordered processing, sort on created_atin the envelope — it's assigned when the event is generated, not when it's delivered.
Next steps
- Webhooks overview
- Signing — verify the HMAC header before processing
- Events — payload shape for each event type