Webhooks
An endpoint is an HTTPS URL of yours and a list of event types, or an empty list for everything. The URL names port 443 or no port at all: webhooks are delivered to port 443 only, and a URL with any other port is refused with unsafe_url. 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; for the three dispute events, the case under data.dispute. Beside the record ride a few facts stored when the event happened (which transition, why a refund, how low the stock). Every event's data is published field by field under Events in the reference, and nothing is sent that is not listed there. 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 at its next failed attempt (on this schedule about the tenth, four days in), 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.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 ended; the outcome is in the payload. Both carry the case under dispute, as GET /v1/orders/{id}/dispute returns it. |
order.dispute_updated | The buyer added a statement, made an offer, or declined or withdrew one. Never your own moves. |
order.message | The buyer or the system wrote in the thread. Never your own words. |
listing.low_stock, listing.sold_out | The stock crossed your alert quantity or reached zero, by a sale, a stock edit, a sheet line or a withdrawal of codes. |
listing.taken_down, listing.reinstated | An operator removed a listing, or put it back. |
account.trading_status_changed | Your account was suspended or reinstated. |
account.selling_changed | Your listings were hidden from buyers by the platform, or returned: paid orders ran out unanswered (missed_orders), or a restricted account reached its open-order limit (order_limit). Resuming after missed orders is done by a person in the seller dashboard, never by a key. Holiday mode is your own switch and does not raise this. |
ping | The test button. |
Every order event that is a change of state carries seq, the transition's number in one series across the whole platform (order.message and order.dispute_updated are not transitions and carry none). Order deliveries by it when they arrive out of turn, and compare it with the order's last_event_seq to see whether what you hold is behind what happened.
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.