Skip to main content
Polling works and is the simplest thing that can possibly work. But a checkout can sit parked on a user action for ten minutes, and polling a run that is doing nothing is pure waste — so status transitions can also be pushed to you. Webhooks tell you that something changed. They deliberately do not carry the checkout object: the payload is small and cheap to sign, and you fetch the current state yourself. A webhook is a doorbell, not a delivery.

Registering an endpoint

Response (201)
secret is returned exactly once, here. Store it before you close the terminal — it is what you verify every future delivery with.
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_actionrunningawaiting_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 carries x-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
Wired into Express, with the raw body preserved:
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

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.
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.
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.
An awaiting_user_action event tells you a question exists, not what it is. Fetch the checkout and read pendingUserAction.
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.