Webhooks (push events)
Webhooks are the realtime channel ProphetX exposes — there is no websocket option, and ProphetX would prefer ISVs not poll the REST API for state changes. Register a receiver and ProphetX pushes async events — contract updates, contract settlements, market-order state changes, parlay state changes, fixtures lifecycle (sport events, tournaments, markets), and market-selection liquidity snapshots — to a URL you operate. You register one base URL per ISV, and ProphetX appends a fixed path to it for each event type.
Orderbook updates. If you're looking for orderbook / depth-of-market data, that's the
MarketSelectionEventon/push/market-selections(section 4.9). The event structure mirrors the market-maker websocket feed; only the transport differs (webhook instead of websocket).
Auth:
- On the management endpoints in this doc: ISV-only JWT (see Authentication).
- On inbound webhook deliveries to you: ProphetX-issued JWT signed by the push service. You should verify it on your end before trusting the payload.
1. Registration endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /private/v1/push/register | { "baseUrl": "https://partner.example.com" } — register your receiver root. |
GET | /private/v1/push/register | Returns the current registration if one exists. |
DELETE | /private/v1/push/register | Stop receiving. Effective immediately. |
POST | /private/v1/push/test | { "type": "contract" | "contract_settlement" | "market_order" | "parlay" | "event" | "tournament" | "market" } — fires a test message and reports round-trip latency. Market-selection webhooks cannot be triggered from this endpoint yet. |
POST /push/register returns:
201on success.409if you're already registered. To change the URL:DELETEfirst, then re-POST.
There is no per-event-type subscription. Once you're registered, you receive all event types.
POST /push/test response shape:
{ "success": true, "latencyMs": 142, "error": null }error is populated when the test couldn't reach you at all (connection refused, timeout).
2. What your receiver must implement
Your baseUrl must expose these paths:
| Method | Path | Why |
|---|---|---|
GET | /health | Called before resuming delivery after a pause. Return 200 when ready. |
POST | /push/contracts | ContractEvent — contract created / updated / deleted. |
POST | /push/contract-settlements | ContractSettlementEvent — contract settled. |
POST | /push/market-orders | MarketOrderEvent — market order created / updated / deleted. |
POST | /push/matched-orders | MatchedOrderEvent — fires once per fill against a resting order. op is "created" or "test" only. |
POST | /push/parlays | ParlayEvent — parlay created / updated / deleted. |
POST | /push/events | SportEventEvent — sport-event lifecycle (status / schedule changes). |
POST | /push/tournaments | TournamentEvent — tournament create / update / delete. |
POST | /push/markets | MarketEvent — market metadata (status / strike changes). Lightweight; for liquidity use /push/market-selections. |
POST | /push/market-selections | MarketSelectionEvent — liquidity snapshots. op is "updated" or "test" only. |
All POST events share one envelope.
3. The event envelope
{
"id": 8291,
"op": "created" | "updated" | "deleted" | "test",
"timestamp": "2026-04-01T12:00:00Z",
"data": { "...": "event-type-specific payload" }
}idis monotonically increasing. Use it to dedupe (you may receive duplicates on retry) and to order events (lateridwins).op = "test"indicates a test message sent viaPOST /push/test. Treat it as a no-op or a connectivity probe; don't apply state changes.
4. Event payloads
4.1 ContractEvent → POST /push/contracts
ContractEvent → POST /push/contracts{
"id": 8291,
"op": "updated",
"timestamp": "...",
"data": {
"eventId": 999999,
"marketId": 219,
"outcomeId": 4,
"contractId": "8315d4bc44d06eef736959e0b214c170",
"name": "Winner (incl. overtime)",
"type": "moneyline",
"price": -110.0,
"adjustedPrice": -115.0,
"strike": 0.0,
"status": "active",
"favourite": false,
"updatedAt": "2026-04-01T12:00:00.000Z"
}
}op semantics for contracts:
created— new contract, or new price available.updated— status orfavouritechanged.deleted— contract no longer available.
status is one of active, suspended, settled. price and adjustedPrice may be null if no liquidity exists yet.
4.2 ContractSettlementEvent → POST /push/contract-settlements
ContractSettlementEvent → POST /push/contract-settlements{
"id": 9001,
"op": "created",
"timestamp": "...",
"data": {
"contractId": "8315...",
"result": "profit",
"eventId": 999999,
"marketId": 219,
"outcomeId": 4,
"strike": 0.0
}
}result is one of profit, loss, push, void. This is the signal to mark the relevant orders' winningStatus in your own database.
4.3 MarketOrderEvent → POST /push/market-orders
MarketOrderEvent → POST /push/market-orders{
"id": 9100,
"op": "updated",
"timestamp": "...",
"data": {
"id": "isv_xyz",
"userId": "...",
"contractId": "8315...",
"quantity": 24.50,
"unfilledQuantity": 0,
"filledQuantity": 24.50,
"refundedQuantity": 0,
"expectedAveragePrice": -110,
"currentAveragePrice": -108,
"nextAveragePrice": -107,
"settlementStatus": "tbd",
"fillStatus": "filled",
"status": "completed",
"createdAt": "...",
"updatedAt": "..."
}
}Same shape as GET /private/v1/market-orders/{refId}, wrapped in the envelope. The data.id here is the order's refId.
4.4 MatchedOrderEvent → POST /push/matched-orders
MatchedOrderEvent → POST /push/matched-ordersFires once per fill against a resting order. Distinct from MarketOrderEvent (which tracks the lifecycle of a single submitted order) — this event is the per-fill signal you'd reconcile against your own trade ledger. op is "created" or "test" only; fills don't update or delete.
{
"id": 9250,
"op": "created",
"timestamp": "...",
"data": {
"contractId": "8315d4bc44d06eef736959e0b214c170",
"eventId": 999999,
"marketId": 219,
"outcomeId": 4,
"strike": 2.5,
"originStrike": -2.5,
"price": -110.0,
"fillPrice": -108.0,
"filledQuantity": 50.0,
"sequenceNumber": 42
}
}Field meanings:
price— the price the resting order was originally placed at.fillPrice— the price this specific fill matched at (may improve onprice).filledQuantity— quantity matched in this fill.strike— the handicap/spread for the contract (0 for moneyline markets).originStrike— the home-team-perspective strike for spreads/totals.nullon moneyline.sequenceNumber— monotonically increasing. Use it to order fills chronologically when receiving multiple events on the same contract.
/push/testdoesn't currently fire matched-order events. TheTestPushRequest.typeenum iscontract | contract_settlement | market_order | parlay | event | market | tournament— nomatched_order(ormarket_selection). You'll only see realMatchedOrderEvents once a fill actually occurs.
4.5 ParlayEvent → POST /push/parlays
ParlayEvent → POST /push/parlaysFires when a parlay is created (i.e. after confirm) or updated (fill state changes, settlement, etc.).
{
"id": 9300,
"op": "updated",
"timestamp": "...",
"data": {
"id": "00000000-0000-0000-0000-000000000000",
"userId": "...",
"settlementStatus": "profit",
"settledAt": "...",
"createdAt": "...",
"updatedAt": "..."
}
}settlementStatus is one of profit, loss, push, tbd. For the full parlay state (fill status, fee, quantities, legs), call GET /private/v1/parlays/{parlayId} — the webhook is a lightweight signal that something changed, not a complete snapshot. See Parlays (2–12 leg trades).
4.6 SportEventEvent → POST /push/events
SportEventEvent → POST /push/events{
"id": 9400,
"op": "updated",
"timestamp": "...",
"data": {
"id": 20023797,
"status": "live",
"startTimeTbd": false,
"sportId": 2,
"categoryId": 0,
"name": "Detroit Pistons at Cleveland Cavaliers",
"scheduled": "2026-05-15T23:00:00Z",
"tournamentId": 132,
"seasonId": null,
"roundId": null,
"updatedAt": "..."
}
}status values: not_started, live, suspended, ended, closed, cancelled, interrupted, postponed, abandoned, delayed. Fires when an event's status flips (e.g. into live), when scheduling changes, or when it's removed. Use it to drive UI like "this event is in-play now".
4.7 TournamentEvent → POST /push/tournaments
TournamentEvent → POST /push/tournaments{
"id": 9500,
"op": "created",
"timestamp": "...",
"data": {
"id": 132,
"name": "NBA",
"sportId": 2,
"categoryId": 0,
"updatedAt": "..."
}
}Tournament-level CRUD. Most ISVs will only care about created (a new tournament appeared and you may want to surface it in your catalog) and deleted.
4.8 MarketEvent → POST /push/markets
MarketEvent → POST /push/markets{
"id": 9600,
"op": "updated",
"timestamp": "...",
"data": {
"id": 186,
"eventId": 20023797,
"status": "suspended",
"strike": 0,
"name": "Moneyline",
"updatedAt": "..."
}
}status values: active, canceled, inactive, settled, skipped, suspended. This is metadata only — no prices, no resting orders. For liquidity, listen on /push/market-selections (next).
4.9 MarketSelectionEvent → POST /push/market-selections
MarketSelectionEvent → POST /push/market-selectionsThe liquidity feed. Fires whenever the resting orders on a market change — new orders posted, fills, voids, or cancels. op is always "updated" (or "test" for the connectivity probe) — there's no create/delete for selections.
{
"id": 9700,
"op": "updated",
"timestamp": "...",
"data": {
"eventId": 20023797,
"tournamentId": 132,
"market": {
"id": 186,
"name": "Moneyline",
"type": "moneyline",
"strike": 0,
"sequenceNumber": 12483,
"selections": [
[
{
"outcomeId": 4,
"name": "Cleveland Cavaliers",
"contractId": "8315d4bc44d06eef736959e0b214c170",
"strike": 0,
"price": -110,
"quantity": 50,
"value": 45.45,
"updatedAt": "..."
}
],
[
{
"outcomeId": 5,
"name": "Detroit Pistons",
"contractId": "abc123...",
"strike": 0,
"price": +100,
"quantity": 25,
"value": 25,
"updatedAt": "..."
}
]
]
}
}
}Notes on the shape:
selectionsis a list of lists — same shape asGET /private/v1/markets. Each inner list is one side of the book, ordered best-price-first, capped at 10.sequenceNumberis per-market and monotonic. If you receive an event with asequenceNumberlower than the last one you processed for the samemarket.id, drop it — it's an out-of-order delivery.- This is a snapshot, not a delta. Each event replaces your view of the market's resting liquidity entirely.
- Test-trigger for this path is not yet supported by
POST /push/test. Verify connectivity using the other event types and trust that real traffic will route correctly once registered.
5. A small naming inconsistency: fillStatus
fillStatus- Private API (
GET /market-orders/{refId}.fillStatus):filled|open|partial - Webhook (
MarketOrderEvent.data.fillStatus):filled|resting|partial
It's the same underlying state, just a different word for "not yet filled." Treat open and resting as synonyms.
6. Delivery, retries, and pausing
| Behavior | Default |
|---|---|
| Delivery timeout | 5 seconds — your receiver must respond 2xx within this window. |
| Retries | Non-2xx responses and timeouts are retried with a 10s delay. |
| Error threshold | After 3 consecutive failures, the receiver is paused and ProphetX stops sending. |
| Resume | ProphetX polls GET /health; when it returns 200, delivery resumes. |
| Max message age | 24 hours. Messages older than this are dropped, not delivered. |
7. Verifying inbound calls came from ProphetX
Each delivery carries a JWT signed by the push service (Ed25519). The push service's public key (or JWK endpoint) is provisioned by ProphetX out of band. Your handler should verify:
- Signature against the configured public key.
issmatches the expected push-service issuer.audis your registered baseUrl (or otherwise as configured).exp/nbfare valid against your clock.
Reject anything that doesn't pass — return non-2xx and ProphetX will retry. Don't act on unverified payloads.
8. Receiver implementation checklist
-
GET /healthreturns 200 when ready (and 5xx when you've intentionally paused). - All eight POST paths are wired to handlers that parse the same envelope.
- Inbound JWT verified on every request.
- Events deduped on
id— a higheridfor the same entity wins. - For
/push/market-selections, additionally drop events with an out-of-orderdata.market.sequenceNumber. - Handlers are idempotent — the same event may arrive more than once.
- Responses go out in under 5 seconds; long-running work happens off the request path.
-
op: "test"is recognized and never applied to real state.
9. Curl
BASE="https://isv-api.staging.prophetx.dev/private/v1"
# Register
curl -X POST "$BASE/push/register" \
-H "Authorization: Bearer $JWT_ISV" \
-H "Content-Type: application/json" \
-d '{ "baseUrl":"https://partner.example.com" }'
# Check
curl "$BASE/push/register" -H "Authorization: Bearer $JWT_ISV"
# Test each event type (market-selection isn't supported by /push/test yet)
for t in contract contract_settlement market_order parlay event tournament market; do
curl -X POST "$BASE/push/test" \
-H "Authorization: Bearer $JWT_ISV" \
-H "Content-Type: application/json" \
-d "{\"type\":\"$t\"}"
done
# Unregister
curl -X DELETE "$BASE/push/register" -H "Authorization: Bearer $JWT_ISV"