Skip to main content

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 payoutprocessing, done, postponed, rejected and aml_rejected. You never register per status.

Which statuses fire

EntityDelivers on
depositevery status change, including aml_processing and processing
withdrawalevery status change, including processing
orderevery state except wait
tradeeach fill
marketa 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.

How to trust a delivery

The token in the headers is what identifies a delivery as genuine, so two rules apply:

  1. Serve the endpoint over HTTPS only, so the token is never sent in cleartext.
  2. 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.
No retries, and no ordering

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. processing can land after done for 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.