POST to an HTTPS endpoint you register.
Payloads use the same IDs and shapes as the REST reads, so you can mix webhooks and REST calls. Webhook payloads documents every event type with an example.
Trade and feed events cover the traders in the partner feed: both their signals and their messages must be publicly visible, and traders excluded from the feed for editorial reasons produce no events. Some traders returned by
/events therefore never produce webhook events.
Register an endpoint
You need an active partner account. You can register up to five endpoints, either in the Centaur app under Settings > Webhooks, or over REST with an API key that has thewebhooks.manage scope:
whsec_ followed by 43 characters. Centaur shows the secret only once, so store it where your receiver can read it. The same routes also answer under /v1/webhooks.
The URL must use HTTPS, must not contain a username or password, and its host must already resolve to a public address. Centaur rejects hosts that are or resolve to private, loopback, link-local, or cloud metadata addresses, at registration and before every delivery.
Choose events and filters
eventTypes lists the types the endpoint receives. Choose at least one. filters narrows trade and feed events. Positioning events ignore filters and always carry the whole view.
Each filter takes up to 100 values. An omitted filter matches everything. Values within a filter match any. Different filters must all match, and
assetIds and assetClasses must match the same asset. A feed item matches when one of its current or removed events matches. Filter changes apply only to events detected afterward.
maxInFlight caps how many deliveries Centaur sends to the endpoint at once, from 1 to 50. The default is 5.
Receive deliveries
Every delivery is one JSON object, never an array, with an envelope around the payload. Webhook payloads describes the envelope and headers. Follow four rules:- Deduplicate on the event
id. Delivery is at least once. A retry, a replay, or a recovered delivery repeats the sameid. - Do not assume order. Deliveries run in parallel and failed ones retry later, so they can arrive out of
sequenceorder. For feed items,currentis the message’s state after the change. - Return a 2xx fast. Store the event, respond, then process it. An attempt fails if your status does not arrive within 10 seconds, including DNS lookup and connection, or if it is outside 2xx. Redirects are not followed, so a 3xx is a failure.
- Verify the signature on the raw body, before you parse the JSON.
schemaVersion change.
Verify signatures
Deliveries do not come from fixed IP addresses, so you cannot allowlist Centaur by IP. The signature proves a request came from Centaur. Verify it on every request. TheCentaur-Signature header holds a timestamp t and one or more v1 signatures. Each v1 is the lowercase hex HMAC-SHA256 of the string <t>.<raw body>, keyed by the whole secret string, including the whsec_ prefix. Do not base64-decode the secret.
- Read
tand everyv1from the header. - Reject the delivery when
tis more than 5 minutes from your clock. This stops an old request from being replayed. - Compute the HMAC with each secret you hold. Accept the delivery when any
v1matches, using a constant-time comparison.
express.raw({ type: 'application/json' }) and parse req.body only after verifying it.
After you rotate the secret, every delivery carries one v1 for the new secret and one for the previous secret for 24 hours. Deploy the new secret within that window. Accepting both secrets in the meantime means no delivery fails verification. Rotating again within the 24 hours retires the oldest secret at once, so only the two newest secrets ever sign.
Test vectors
Check your code against these fixed values. They uset = 1759658400, a time in the past, so pass that time as the current time when you test.
During the rotation from the previous secret to the new one, the header is:
verifyCentaurSignature(header, body, [newSecret], 1759658400) and verify_centaur_signature(header, body, [previous_secret], now_seconds=1759658400) both return true. Changing one byte of the body makes them return false. You can reproduce a signature without any code:
Send a test delivery
POST /v2/webhooks/{id}/test sends the trade_event.created example from Webhook payloads to that endpoint only, whatever its event types and filters. It has a new event id, the current time as occurredAt, and a top-level test: true marker inside the signed body. Real events never carry test, including real events on staging. A test is signed and retried like any delivery and keeps its marker on retries and replays, including tests created before the marker existed. The response gives its eventId, deliveryId, and status: PENDING, or HELD when the endpoint is paused or disabled.
Manage endpoints
These routes take the endpointid. The API reference has every request and response.
Pause and resume work on active and paused endpoints. Enable works on disabled endpoints. Pausing or resuming a disabled endpoint, or enabling a paused one, returns
409.
A delivery’s status is one of:
PENDING: waiting for its first or next attempt.IN_FLIGHT: being sent.SUCCEEDEDHELD: kept unsent while the endpoint is paused or disabled.DEAD: every attempt failed. See Retries.
Retries and dead letters
A delivery succeeds when your endpoint answers with a 2xx within 10 seconds. Otherwise Centaur retries it with growing, randomized waits: six attempts over about two hours. A retry keeps the eventid, sequence, and payload, with a fresh signature timestamp.
When the sixth attempt fails, the delivery becomes a dead letter with status DEAD. A 410 Gone, or a host that resolves to an address Centaur refuses, makes it a dead letter at once and disables the endpoint. Dead letters stay in the endpoint’s deliveries, and you can replay them.
Disabled endpoints
Centaur disables an endpoint, and setsdisabledReason, when:
- it answers
410 Gone(410_gone). Use this status to tell Centaur to stop. - at least 50 attempts in a row have failed and it has had no success for about two hours (
sustained_failure). The two-hour clock restarts when you enable or resume the endpoint. - its host starts resolving to an address Centaur refuses (
unsafe_url).
disabledReason is the reason you gave, or partner_request. Centaur may also disable an endpoint with a note.
While an endpoint is paused or disabled, Centaur keeps creating its deliveries as HELD for the seven-day retention window and sends none of them. Deliveries already waiting for an attempt are held too. Resuming a paused endpoint sends its held deliveries. Enabling a disabled endpoint does not: replay the range you need.
Replay
POST /v2/webhooks/{id}/replay sends past deliveries again. Give the start of the range as a time or as a sequence number:
fromselects deliveries created at or after that time, at most seven days ago. Older events are deleted, so reconcile beyond that with REST reads.fromSequencereaches only deliveries that were already attempted. Held deliveries that were never attempted have no sequence yet, so release them withfrom.- Held deliveries in the range are released. Succeeded and dead deliveries are sent again as new deliveries with the same event
idand a newsequence, so deduplicate onid. Deliveries already waiting or in flight are left alone. - The endpoint must be active. Resume or enable it first.
- A replay that resends deliveries can run once a minute per endpoint. Otherwise the API returns
429withRetry-After.
released, the held deliveries released, and reenqueued, the deliveries sent again.
Limitations
- Detection is periodic. Centaur checks for trade and feed changes every 30 seconds and recalculates each positioning view at most every 1, 5, 10, or 15 minutes, depending on the view. Events arrive some time after the change.
- Only changes after registration. An endpoint receives changes detected after it was created. It can receive a
trade_event.deleted,feed_item.updated, orfeed_item.deletedfor an item it never saw created. - Corrections can reissue unchanged events under new IDs. When a trader’s history is corrected, Centaur deletes that trader’s later events and creates them again with new IDs, often with the same content. You receive
trade_event.deletedandtrade_event.createdpairs withorigin: replacement, and onefeed_item.updatedper affected message. Position IDs can change too, even on unchanged trades from later messages.origin: replacementis a same-message, same-pass hint, not a guaranteed correction label or an old-to-new ID map, so do not use it to track position identity. A pair with the same content is an ID change, not a new signal. One correction can send a burst as large as the trader’s history after the corrected time, andmaxInFlightcaps how many of those requests reach you at once. A very large correction can span several detection passes, so some re-created events carryorigin: newand a message can get more than onefeed_item.updated. To receive fewer events, subscribe to feed items only, wherecurrentshows that nothing changed. removedis not a reversal. An event inremoved, or atrade_event.deleted, means stop treating that event as true. It does not mean the trader reversed the trade.- Short-duration positions are delivered.
/events,/feed, and/positionshide positions that lasted 60 seconds or less. Webhooks still deliver their events. - No events for source edits or deletions. Source text is the version Centaur extracted the trade from: the original message, or its earliest edit when the original is unavailable. Later edits and deletions produce no event and do not remove extracted trades on their own.
- No events for visibility changes. When a trader becomes visible or hidden, or joins or leaves the feed’s editorial exclusions, existing items produce no events. Use a fresh REST read to reconcile.
- No history window for feed items. A late event on an older message produces a feed item event even when the message is outside the feed’s seven-day window.
- Prices can be missing. Trade prices may be null. A later price update does not send a follow-up trade webhook.
- Positioning depends on prices. Early Opportunities leaves out candidates without the prices it needs, so partial price failures can change membership and send alerts. Delivery does not hold back old rows or suppress those changes. Check the payload’s
coverageandmissingPriceGroups.