Subscribing
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: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.
compare_digest is the timing-safe compare:
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:
One subscription per address and event set
Subscribing the same URL to exactly the same events twice is refused with409 conflict, naming the subscription already doing it:
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