Registering an endpoint
Response (201)
url must be https://. http://localhost and http://127.0.0.1 are accepted so you can
develop against a local listener; nothing else over plain HTTP is.
Multiple endpoints per tenant are supported and each gets its own secret. Delivery is
per-endpoint, so a broken endpoint retries without re-delivering to the healthy ones.
List → 200
lastDeliveryStatus is the raw HTTP status your endpoint last returned. It is the fastest
way to notice you have been 500ing for a day. Revoked endpoints disappear from the list and
stop receiving deliveries immediately.
Read the array from data, the field the checkout and buyer-profile lists also use. The same array
is returned as endpoints too — the original field name, kept for existing callers — and
nextCursor is always null here because the collection is not paged.
The event
One event type exists today.string
Always
"checkout.status_changed". Switch on it anyway — more types will come and your
handler should ignore what it does not recognise.string
Fetch
GET /v1/checkouts/{checkoutId} for the actual state.string
The status the checkout just moved to.
string | null
Set only when
status is failed. One of max_cost_exceeded, user_cancelled,
user_action_expired, automation_failed.string
Omitted when the checkout is not tagged to an end user. Your own id for that person when it
is. See End-user isolation.An endpoint is tenant-wide — it receives every end user’s transitions — so this is what lets you
route a delivery to the right person: notify that user, wake that user’s job, write to that user’s
row. Without it the only way to find out whose run this was would be a call back to
GET /v1/checkouts/{checkoutId} with a tenant-wide key, or your own map from checkout id to user
kept since creation.string
When the transition was dispatched — stamped once, so a retried delivery carries the
original event time, not the retry time.
subject is a routing hint, not an authorisation decision. It is your own string coming back to
you, and the delivery is only trustworthy at all once you have
verified the signature.Which transitions fire
A checkout that asks two questions produces
awaiting_user_action → running →
awaiting_user_action, because answering an action returns the run to running. Expect
repeated visits to both.
Delivery is best-effort by design: a webhook must never fail or slow down a checkout
transition. Fan-out is enqueued and never awaited on the checkout’s critical path. If your
endpoint is down for the whole retry window, the transition still happened — polling is
always the source of truth.
Verifying the signature
Every delivery carriesx-checkoutvia-signature in the Stripe style:
v1 is HMAC-SHA256(secret, "<t>.<raw body>"), hex-encoded. Sign against the raw request
body bytes, before any JSON parse — re-serializing the parsed object will change key order
or whitespace and the HMAC will not match.
verify.js
server.js
Delivery behaviour
10 seconds
A delivery that has not responded in 10 seconds is aborted and treated as a failure.
Return
2xx immediately and process asynchronously.3
A non-
2xx response or a connection failure is retried by the durable dispatcher, with
backoff. Retries are per-endpoint: a delivery that already succeeded is never repeated
because a sibling endpoint failed.any 2xx
Anything outside
2xx counts as a failure and is retried. 410 Gone will not
auto-disable your endpoint — revoke it explicitly.Caveats you must design around
No ordering guarantee
No ordering guarantee
Each transition is dispatched as its own event and retried independently. A retried
running can arrive after succeeded.Never reconstruct state by replaying the sequence of webhooks. Use the event as a
trigger, fetch GET /v1/checkouts/{id}, and act on what that returns. createdAt gives
you a dispatch time you can compare if you need to discard an obviously stale event.No event id, no idempotency key
No event id, no idempotency key
The payload has no unique event identifier. A retry is byte-identical to the original
delivery, so you cannot dedupe on the body alone.Make your handler idempotent against
(checkoutId, status) instead, or simply make it
idempotent against the fetched checkout’s current state — which is the right design
anyway given the ordering caveat.Duplicate deliveries are normal
Duplicate deliveries are normal
Two consecutive
awaiting_user_action events for one checkout are expected behaviour,
not a bug — see above. And a delivery your endpoint processed but timed out on will be
retried.The event does not name the action
The event does not name the action
An
awaiting_user_action event tells you a question exists, not what it is. Fetch the
checkout and read pendingUserAction.Secrets are shown once and cannot be rotated in place
Secrets are shown once and cannot be rotated in place
There is no rotate endpoint. To rotate: register a second endpoint at the same URL,
accept either secret during the overlap, then revoke the first. Deliveries go to every
active endpoint, so during the overlap you receive each event twice — which your
idempotent handler already tolerates.