Webhooks¶
A webhook subscription delivers account, order, position and transaction events
to an endpoint you host, so your systems learn about changes without polling
for them. The event body is exactly the envelope the
WebSocket events channel carries, with
the same sequence number.
Every route on this page requires the webhooks:manage scope.
Early access
Webhook delivery is being finalized alongside the rest of this API. The subscription management endpoints below are stable; the two headers that identify and sign a delivery are named in your integration pack rather than here, because they are the last part of the contract still being settled. The signing scheme below is settled, and is what you should build against.
Create a subscription¶
curl -sS -X POST https://platform-api.example.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"url": "https://hooks.acme.example/knockout",
"event_categories": ["transaction", "account"]
}'
{
"id": 7741,
"url": "https://hooks.acme.example/knockout",
"brand_id": null,
"event_categories": ["transaction", "account"],
"disabled": false,
"disabled_reason": null,
"last_delivered_seq": 0,
"created_at": 1753634000123,
"secret": "whsec_..."
}
The signing secret is shown once
secret appears only in this response and in the response to a rotation.
No other endpoint returns it, and there is no field on a subscription that
could carry it. Store it when you create the subscription.
Requirements on url:
- It must be
https://. - It must resolve exclusively to public addresses. A private, loopback, link-local or cloud-metadata target is refused when you create the subscription, and re-checked before every delivery.
event_categories must contain at least one of account, order, position,
transaction.
If your credential is bound to a brand, the subscription inherits that binding and receives only that brand's events. A firm-wide credential creates a firm-wide subscription.
Manage subscriptions¶
| Request | Does |
|---|---|
GET /api/v1/webhooks |
Lists the subscriptions this credential can see. |
GET /api/v1/webhooks/{id} |
Reads one. |
POST /api/v1/webhooks/{id}/rotate-secret |
Issues a new signing secret and returns it once. |
POST /api/v1/webhooks/{id}/disable |
Stops delivery. |
An id that belongs to another firm answers 404, exactly like an id that never
existed — no endpoint tells you which.
last_delivered_seq on a subscription is the sequence number of the last event
successfully delivered to it. It is the same number the WebSocket events
channel reports, so you can compare the two, or resume over the socket from
where deliveries stopped.
Verifying a delivery¶
A delivery is a POST with the event envelope as the JSON body, plus two
headers: one carrying the signature and one carrying the subscription id.
Verify every delivery before you act on it:
- Read the signature header. It is a comma-separated list of
key=valuepairs:tis the delivery timestamp in Unix epoch milliseconds, and eachv1is a signature. - Build the signed message as the timestamp, a literal
., and the raw request body — bytes as received, before any JSON parsing or re-serialization. - Compute
HMAC-SHA256of that message using your signing secret, and hex-encode it in lower case. - Compare it against each
v1value using a constant-time comparison. A match on any one of them is a pass. - Reject the delivery if
tis further than five minutes from your own clock. That check is the replay defense, and skipping it undoes most of the value of signing.
t=1753634000123,v1=8f3b...c1
During a rotation the header carries two v1 values — one under the new
secret and one under the previous. A receiver that has already picked up the
new secret and one that still holds the old both verify, so you can rotate
without a coordinated deploy.
Retries and failure¶
A delivery that fails is retried with a widening backoff — 30 seconds, then 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours, and 12 hours — for up to eight attempts, so the last one lands about 21 hours after the first.
Design your endpoint accordingly:
- Be idempotent. Delivery is at-least-once, so the same event can arrive
twice. Key your side effects on the envelope's
id. - Answer quickly. Return a
2xxas soon as you have durably accepted the event, and do the work afterwards. A response that takes longer than 15 seconds counts as a failure and is retried. - Any non-
2xxis a failure and will be retried.
If an endpoint stays down long enough to exhaust the attempts, do not try to
reconstruct what you missed from your own logs: last_delivered_seq on the
subscription tells you exactly where delivery stopped, and the
WebSocket events channel will replay
from that point with after_seq — within its 7-day window. Past that, re-sync
with the bulk reads.
A subscription is disabled automatically if its target stops being a valid
public address; disabled_reason says so, and delivery does not resume until
the subscription is recreated against a working URL.
Choosing between webhooks and the socket¶
They carry the same events. Use whichever fits the consumer:
| Webhooks | WebSocket events |
|
|---|---|---|
| Direction | We call you | You call us |
| Needs a public endpoint | Yes | No |
| Catch-up after downtime | Automatic, through retries | You ask, with after_seq |
| Best for | Back-office systems and integrations | Live dashboards and trading front ends |
There is no reason to run both for the same purpose. Running both for different purposes is fine: they are independent subscriptions to the same feed.