Webhooks

Delivery and retries

How many times we try, what marks an endpoint unhealthy, and how to make your handler safe

A delivery succeeds when your endpoint answers 2xx within 10 seconds. Anything else (a 4xx, a 5xx, a timeout, a connection error) is a failure and is retried.

Acknowledge first and work afterwards: read the body, verify the signature, return 200, then do the slow part. A handler that calls three other APIs before responding will eventually time out and be retried, which produces duplicates rather than reliability.

Retries

We attempt each delivery up to four times: the first immediately, then after 1s, 5s and 30s. After the fourth failure that delivery is given up on. It stays visible in the delivery log.

Unhealthy endpoints

After 10 consecutive failures against the same endpoint we:

  1. mark the endpoint unhealthy,
  2. disable it, so we stop sending, and
  3. email the workspace owner.

A single success resets the counter, and test deliveries never count toward it. Re-enable the endpoint from Settings, Developer once the receiving end is fixed.

Be idempotent

Delivery is at-least-once: a retry after a timeout that actually succeeded on your side, or a resend, means the same event can arrive twice. Key your handler on event_id:

async function handle(event) {
  if (await seen(event.event_id)) return;   // already processed
  await process(event);
  await remember(event.event_id);
}

event_id is stable for one logical event across retries and across endpoints. id changes per delivery attempt, so use it for tracing a single attempt in your logs, never for deduplication.

Ordering

Events are not ordered. message.sent for a second message can arrive before the first, and conversation.ended can beat a message.sent that preceded it. Where order matters, use the timestamps inside data.

URL rules

We re-validate the destination on every delivery, not only when you save it:

  • https is required in production.
  • Localhost, private ranges, link-local and cloud metadata addresses are rejected.
  • Redirects are not followed; a 3xx counts as a failed delivery.

An endpoint that starts resolving to a private address, for instance because a DNS record changed, stops receiving deliveries. To develop locally, use a tunnel that gives you a public https URL.

Next steps