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:

Retry schedule

Retries are scheduled at increasing intervals from the initial attempt:

AttemptDelay from previousApprox. elapsed
1immediate0
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:

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