Skip to content

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:

  1. Read the signature header. It is a comma-separated list of key=value pairs: t is the delivery timestamp in Unix epoch milliseconds, and each v1 is a signature.
  2. Build the signed message as the timestamp, a literal ., and the raw request body — bytes as received, before any JSON parsing or re-serialization.
  3. Compute HMAC-SHA256 of that message using your signing secret, and hex-encode it in lower case.
  4. Compare it against each v1 value using a constant-time comparison. A match on any one of them is a pass.
  5. Reject the delivery if t is 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 2xx as 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-2xx is 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.