Receiving Deliveries
Everything a receiving endpoint has to handle: how to tell a genuine delivery from a forged one, which events arrive, and what the delivery contract does not guarantee.
One subscription per entity
A subscription is registered per action, not per status. One withdrawal
subscription delivers every status of every payout — processing, done,
postponed, rejected and aml_rejected. You never register per status.
Which statuses fire
| Entity | Delivers on |
|---|---|
deposit | every status change, including aml_processing and processing |
withdrawal | every status change, including processing |
order | every state except wait |
trade | each fill |
market | a change to a market |
So a receiver must expect intermediate statuses, not only final ones. The single
exception in the other direction is an order being accepted: there is no wait
delivery, so the response to
Create Order is the only record that the order
exists.
Authenticating a delivery
The token you registered is sent on every request, in two headers at once:
Webhook-Token: YOUR_WEBHOOK_TOKEN
Authorization: Bearer YOUR_WEBHOOK_TOKEN
If your endpoint is configured for basic auth, the second header is
Authorization: Basic base64(username:token) instead.
Compare the token to the one you registered, on every request, and reject anything that does not match.
The token in the headers is what identifies a delivery as genuine, so two rules apply:
- Serve the endpoint over HTTPS only, so the token is never sent in cleartext.
- Re-read the object through the API before anything irreversible. Treat a delivery as a signal that something changed, and the API as the record of what it changed to.
Delivery is asynchronous, has no sequence number, and is not retried automatically. Two things follow, and both will happen in production:
- A delivery can be lost. If your endpoint is down or answers slowly, that event is gone — nothing re-sends it. Reconcile by polling the API for anything that matters.
- Deliveries can arrive out of order.
processingcan land afterdonefor the same object.
So the receiver must be idempotent and must apply state by comparing
updated_at, not by arrival order:
on delivery:
existing = lookup(payload.id)
if existing and existing.updated_at >= payload.updated_at:
ignore # this is older news than what we already have
else:
apply(payload)
Writing the handler as "set status = payload.status" instead will eventually walk a
customer's payout backwards from done to processing.