Skip to main content
Be told what happened instead of watching for it. Delivery is at least once, which is the only promise worth making over a network we do not control.

Subscribing

The secret is returned once. Store it; it is what makes a delivery verifiable. GET /v1/webhooks lists subscriptions without it, and DELETE /v1/webhooks/{webhook_id} removes one.

Event types

This is a shorter list than the event stream’s on purpose: a webhook is for facts worth waking a system up for, not for watching an agent think.

The payload

data names the object; it never embeds it. A receiver that reads the session afterwards sees current truth. One that trusted an embedded copy would act on a snapshot that was already stale when it was signed — and we would owe you a second schema to keep compatible forever.

Verifying a delivery

Two headers:
The signature is HMAC-SHA256(secret, "{timestamp}.{body}"), hex. The timestamp is inside the signed material, not only beside it — without that, a captured delivery stays valid forever and you have no way to reject an old one.
In Python, the same three rules — compare_digest is the timing-safe compare:
Verify against the raw body, before any JSON parse and re-serialise — a round trip changes key order and whitespace and will not match. Getting the raw body is the part frameworks make hard, and each one has its own way: A verifier that re-serialises the parsed object passes every test you write against your own serialiser and fails against ours. The timestamp is milliseconds, the same units as Date.now(). It is signed as the exact digits sent, so verify with the string you received rather than a number you converted and converted back. Then reject a timestamp far from your own clock:
A few minutes is a reasonable window; we do not pick one for you because your tolerance for clock skew is yours to decide. Dividing by 1000 first — which is what a seconds-shaped example invites — rejects every delivery, and it fails looking exactly like a bad signature.

One subscription per address and event set

Subscribing the same URL to exactly the same events twice is refused with 409 conflict, naming the subscription already doing it:
A duplicate is easy to create by accident — retrying a create that appeared to fail is enough — and what it buys you is every delivery twice, permanently, with nothing anywhere saying so. Deliveries are already at-least-once; doubling them is a different problem. The same address subscribed to a different set of events is a different subscription and is allowed: one endpoint for completions and another for the ones that need a person is a reasonable shape.

Retries

A delivery is owed until it is delivered or given up on. Any non-2xx response, or no response, is a failure. Six attempts over roughly nine hours: long enough that a deploy or a short outage on your side is survivable, short enough that a permanently broken endpoint stops being retried the same day. After that the delivery is marked dead with the reason recorded. A receiver that hangs is abandoned after 10 seconds and retried. Answer quickly and do the work afterwards.

At-least-once delivery

Deliveries can arrive more than once, and can arrive out of order. The queue is rows in a database rather than timers in memory, so a restart still owes what it owed — and the same property means a retry can overtake nothing and a network can duplicate. Make your handler idempotent. The event names an object; read the object.

turn.completed waits for the artifacts

The notification means “come and look”, so it is sent once that turn’s artifacts have finished publishing rather than the instant the turn settles. Without that wait, an integration doing the obvious thing — receive the call, fetch the artifacts — found an empty list, which is indistinguishable from a turn that produced nothing. If publication has not finished after 30 seconds the notification is sent regardless. Read artifacts on the turn to tell the two apart: ready means an empty list is final, pending means come back, and partial means some files were left behind — artifacts_skipped on the turn says which and why. A delivery that never arrives is usually a subscription that was never created, a URL that is not https, or an event name that is not one of the above — see troubleshooting.md for symptoms and errors.md for the refusal shape.

Next

  • events — watch a single session live instead
  • idempotency — deliveries arrive at least once