Skip to main content
Webhooks push changes to your server, so you do not have to poll the feed, events, or Position Intelligence. Each change arrives as one signed JSON 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 the webhooks.manage scope:
The response contains the endpoint and its signing secret: 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:
  1. Deduplicate on the event id. Delivery is at least once. A retry, a replay, or a recovered delivery repeats the same id.
  2. Do not assume order. Deliveries run in parallel and failed ones retry later, so they can arrive out of sequence order. For feed items, current is the message’s state after the change.
  3. 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.
  4. Verify the signature on the raw body, before you parse the JSON.
Ignore fields you do not recognize. Payloads can gain fields without a 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. The Centaur-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.
  1. Read t and every v1 from the header.
  2. Reject the delivery when t is more than 5 minutes from your clock. This stops an old request from being replayed.
  3. Compute the HMAC with each secret you hold. Accept the delivery when any v1 matches, using a constant-time comparison.
Pass the raw body as bytes or as the exact string received. In Express, for example, mount the route with 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 use t = 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 endpoint id. 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.
  • SUCCEEDED
  • HELD: 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 event id, 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 sets disabledReason, 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).
When you disable an endpoint yourself, 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:
  • from selects deliveries created at or after that time, at most seven days ago. Older events are deleted, so reconcile beyond that with REST reads.
  • fromSequence reaches only deliveries that were already attempted. Held deliveries that were never attempted have no sequence yet, so release them with from.
  • Held deliveries in the range are released. Succeeded and dead deliveries are sent again as new deliveries with the same event id and a new sequence, so deduplicate on id. 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 429 with Retry-After.
The response reports 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, or feed_item.deleted for 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.deleted and trade_event.created pairs with origin: replacement, and one feed_item.updated per affected message. Position IDs can change too, even on unchanged trades from later messages. origin: replacement is 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, and maxInFlight caps how many of those requests reach you at once. A very large correction can span several detection passes, so some re-created events carry origin: new and a message can get more than one feed_item.updated. To receive fewer events, subscribe to feed items only, where current shows that nothing changed.
  • removed is not a reversal. An event in removed, or a trade_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 /positions hide 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 coverage and missingPriceGroups.
Last modified on October 22, 2026