Webhooks
An endpoint is an HTTPS URL of yours and a list of event types, or an empty list for everything. Up to five per account. Each has its own secret, shown once when it is created and again only when rotated.
The delivery
A POST with a JSON body: { "id", "type", "created_at", "api_version", "data": { … } }. For an order event data.order is the order exactly as GET /v1/orders/{id} returns it; for a listing event data.listing likewise; for order.message, the message. Four headers ride along:
| Header | Meaning |
|---|---|
Freeport-Signature | t=<unix seconds>,v1=<hex>. Two v1 values for a day after a secret rotation. |
Freeport-Event-Id | The event's id. The same event never arrives under another id, so this is the key to deduplicate on. |
Freeport-Event-Type | The type, so a router need not parse the body. |
Freeport-Delivery-Id | This attempt, for the log in the console. |
Verifying
Compute HMAC-SHA256 with the endpoint's secret over ${t}.${body}, where t is the timestamp from the header and body is the raw bytes exactly as received, not re-serialised. Accept the delivery if any v1 value equals your result, compared in constant time. Refuse a t more than five minutes from now. Then answer any 2xx quickly and do the work afterwards: a delivery waits ten seconds for an answer and never follows a redirect.
Retries
A non-2xx answer or a timeout is retried after one minute, five, thirty, two hours, six, twelve, then daily, ten attempts in all. An endpoint that has accepted nothing for three days is disabled, with an email to the account and a row in the audit log; re-enable it from the console once it is fixed, and replay what it missed from the delivery log. Order events carry seq: ordering across events is not guaranteed, so fetch the order on receipt rather than trusting the body's state to be the latest.
Events
| Type | When |
|---|---|
order.paid | Money is held; the accept clock is running. The one to act on. |
order.accepted, order.delivering, order.delivered, order.completed | The order moving through its states, including moves your own tool made. |
order.cancelled, order.refunded, order.expired | Money went back. Stock is restored. |
order.disputed, order.dispute_resolved | A case opened and decided; the outcome is in the payload. |
order.message | The buyer or the system wrote in the thread. Never your own words. |
listing.low_stock, listing.sold_out | A sale crossed your alert quantity, or took the last unit. |
listing.taken_down, listing.reinstated | An operator removed a listing, or put it back. |
account.trading_status_changed | Your account was suspended or reinstated. |
ping | The test button. |
Where they come from
Every event is written in the same database transaction as the change it describes and dispatched from there. Nothing is emitted from a request handler, so an event never describes a change that rolled back, and a change never happens without its event. That is also why an event can arrive a second or two after the state it names: it is real by then.