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, matched-order fills, parlay state changes, fixtures lifecycle (sport events, tournaments, markets), market-selection liquidity snapshots, wallet transactions, and KYC status changes — 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" | "market_selections" | "transaction" | "kyc_status" } — fires a test message and reports round-trip latency. matched_order cannot be triggered from this endpoint — you'll only see that event type on real activity. |
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 is omitted entirely on success. When present, it's populated either because the test couldn't reach you at all (connection refused, timeout) or because your receiver responded with an unexpected non-2xx status.
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. |
POST | /push/transactions | TransactionEvent — a wallet transaction was recorded. op is "created" or "test" only. |
POST | /push/kyc-status | KycStatusEvent — a user's KYC application was opened or its status changed. |
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,
"priceFormats": {
"american": -110,
"decimal": 1.9091,
"cents": 52.38,
"probability": 52.38
},
"adjustedPrice": -115.0,
"adjustedPriceFormats": {
"american": -115,
"decimal": 1.8696,
"cents": 53.49,
"probability": 53.49
},
"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.
priceFormats and adjustedPriceFormats are present only when the ISV has additional price formats enabled, and omitted when the corresponding price is null. american is canonical and the only format the exchange accepts on input — decimal, cents, and probability are display-only views derived from it and do not round-trip to an exact American price. Do not build order guardrails from derived formats.
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,
"currentAveragePriceFormats": {
"american": -108,
"decimal": 1.9259,
"cents": 51.92,
"probability": 51.92
},
"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,
"priceFormats": {
"american": -110,
"decimal": 1.9091,
"cents": 52.38,
"probability": 52.38
},
"fillPrice": -108.0,
"fillPriceFormats": {
"american": -108,
"decimal": 1.9259,
"cents": 51.92,
"probability": 51.92
},
"filledQuantity": 50.0,
"sequenceNumber": 42
}
}Field meanings:
price— the price the resting order was originally placed at.priceFormats—priceexpressed in every supported format. Present only when the ISV has additional price formats enabled.fillPrice— the price this specific fill matched at (may improve onprice).fillPriceFormats—fillPriceexpressed in every supported format. Present only when the ISV has additional price formats enabled.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 | market_selections | transaction | kyc_status— nomatched_order. 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,
"competitorId": 101,
"name": "Cleveland Cavaliers",
"displayName": "Cleveland Cavaliers",
"contractId": "8315d4bc44d06eef736959e0b214c170",
"strike": 0,
"displayStrike": "0",
"price": -110,
"priceFormats": {
"american": -110,
"decimal": 1.9091,
"cents": 52.38,
"probability": 52.38
},
"displayPrice": "-110",
"adjustedPrice": -115,
"adjustedPriceFormats": {
"american": -115,
"decimal": 1.8696,
"cents": 53.49,
"probability": 53.49
},
"quantity": 50,
"value": 45.45,
"updatedAt": "..."
}
],
[
{
"outcomeId": 5,
"competitorId": 102,
"name": "Detroit Pistons",
"displayName": "Detroit Pistons",
"contractId": "abc123...",
"strike": 0,
"displayStrike": "0",
"price": +100,
"priceFormats": {
"american": 100,
"decimal": 2.0000,
"cents": 50.00,
"probability": 50.00
},
"displayPrice": "+100",
"adjustedPrice": 95,
"adjustedPriceFormats": {
"american": 95,
"decimal": 1.9474,
"cents": 51.35,
"probability": 51.35
},
"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.adjustedPrice/adjustedPriceFormats,competitorId,displayName,displayPrice, anddisplayStrikeare all optional and omitted when not applicable (e.g. no ISV fee configured, or no competitor on this outcome) — same semantics as the equivalent fields onContractEvent.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.
- You can fire a test event for this path via
POST /push/testwith{ "type": "market_selections" }.
4.10 TransactionEvent → POST /push/transactions
TransactionEvent → POST /push/transactionsFires when a wallet transaction is recorded. The ledger is append-only, so op is always "created" (or "test" for the connectivity probe) — there's no update/delete for transactions.
{
"id": 9800,
"op": "created",
"timestamp": "...",
"data": {
"id": "00000000-0000-0000-0000-000000000000",
"userId": "...",
"type": "BET",
"amount": "-24.50",
"balanceBefore": "100.00",
"balanceAfter": "75.50",
"contractId": "8315...",
"refId": "isv_xyz",
"createdAt": "..."
}
}type is one of DEPOSIT, WITHDRAW, APPROVE_WITHDRAW, REJECT_WITHDRAW, BET, CANCEL, PAY, PUSH, VOID, REFUND, ORDER_FEE, REFUND_FEE, COMMISSION, ADJUSTMENT_INCREASE, ADJUSTMENT_REDUCE, JOURNAL_DEPOSIT, JOURNAL_WITHDRAW. userId is absent for ISV-level transactions (e.g. DEPOSIT/WITHDRAW on a STANDING-fund pot). contractId and refId are absent when not applicable to the transaction type.
4.11 KycStatusEvent → POST /push/kyc-status
KycStatusEvent → POST /push/kyc-statusFires when a user's KYC application is opened, and again whenever its status changes.
{
"id": 9900,
"op": "updated",
"timestamp": "...",
"data": {
"userId": "...",
"kycStatus": "FAILURE",
"canRetry": true,
"failReason": "document_unreadable",
"updatedAt": "..."
}
}kycStatus is one of PENDING, PHOTO_VERIFICATION_PROCESSING, SUCCESS, FAILURE, MORTALITY, PEP, OFAC. PENDING and PHOTO_VERIFICATION_PROCESSING are in-flight; the rest are terminal for the current attempt, and only SUCCESS promotes the applicant to a verified user. canRetry is only ever true for FAILURE, and only while retries remain (see KYC testing for the retry cap). failReason is present only for FAILURE, MORTALITY, PEP, and OFAC, and only when the provider supplied one.
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 | Retried with a 10s delay only for a connection-level failure (timeout, refused connection, DNS failure) or a 5xx response. Any other non-2xx status (e.g. 400/401/403/422) is treated as permanent — the message is dropped after one attempt, no retry. |
| Error threshold | After 4 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.audmatches your ISV ID.expis valid against your clock. (There is nonbfclaim on these tokens.)
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 eleven POST paths are wired to handlers that parse the same envelope. A 404 on any of them is treated as a silent no-op by ProphetX — it won't be retried and it won't count toward the error threshold, so a missing handler goes unnoticed. If you don't need an event type yet, wire an empty handler that returns 200 so you can see it in your logs.
- 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.sandbox.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 (matched_order isn't supported by /push/test)
for t in contract contract_settlement market_order parlay event tournament market market_selections transaction kyc_status; 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"Updated 15 days ago
