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 MarketSelectionEvent on /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

MethodPathPurpose
POST/private/v1/push/register{ "baseUrl": "https://partner.example.com" } — register your receiver root.
GET/private/v1/push/registerReturns the current registration if one exists.
DELETE/private/v1/push/registerStop 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:

  • 201 on success.
  • 409 if you're already registered. To change the URL: DELETE first, 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:

MethodPathWhy
GET/healthCalled before resuming delivery after a pause. Return 200 when ready.
POST/push/contractsContractEvent — contract created / updated / deleted.
POST/push/contract-settlementsContractSettlementEvent — contract settled.
POST/push/market-ordersMarketOrderEvent — market order created / updated / deleted.
POST/push/matched-ordersMatchedOrderEvent — fires once per fill against a resting order. op is "created" or "test" only.
POST/push/parlaysParlayEvent — parlay created / updated / deleted.
POST/push/eventsSportEventEvent — sport-event lifecycle (status / schedule changes).
POST/push/tournamentsTournamentEvent — tournament create / update / delete.
POST/push/marketsMarketEvent — market metadata (status / strike changes). Lightweight; for liquidity use /push/market-selections.
POST/push/market-selectionsMarketSelectionEvent — 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" }
}
  • id is monotonically increasing. Use it to dedupe (you may receive duplicates on retry) and to order events (later id wins).
  • op = "test" indicates a test message sent via POST /push/test. Treat it as a no-op or a connectivity probe; don't apply state changes.

4. Event payloads

4.1 ContractEventPOST /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 or favourite changed.
  • 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 ContractSettlementEventPOST /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 MarketOrderEventPOST /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 MatchedOrderEventPOST /push/matched-orders

Fires 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 on price).
  • 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. null on moneyline.
  • sequenceNumber — monotonically increasing. Use it to order fills chronologically when receiving multiple events on the same contract.

/push/test doesn't currently fire matched-order events. The TestPushRequest.type enum is contract | contract_settlement | market_order | parlay | event | market | tournament — no matched_order (or market_selection). You'll only see real MatchedOrderEvents once a fill actually occurs.

4.5 ParlayEventPOST /push/parlays

Fires 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 SportEventEventPOST /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 TournamentEventPOST /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 MarketEventPOST /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 MarketSelectionEventPOST /push/market-selections

The 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:

  • selections is a list of lists — same shape as GET /private/v1/markets. Each inner list is one side of the book, ordered best-price-first, capped at 10.
  • sequenceNumber is per-market and monotonic. If you receive an event with a sequenceNumber lower than the last one you processed for the same market.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

  • 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

BehaviorDefault
Delivery timeout5 seconds — your receiver must respond 2xx within this window.
RetriesNon-2xx responses and timeouts are retried with a 10s delay.
Error thresholdAfter 3 consecutive failures, the receiver is paused and ProphetX stops sending.
ResumeProphetX polls GET /health; when it returns 200, delivery resumes.
Max message age24 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:

  1. Signature against the configured public key.
  2. iss matches the expected push-service issuer.
  3. aud is your registered baseUrl (or otherwise as configured).
  4. exp / nbf are 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 /health returns 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 higher id for the same entity wins.
  • For /push/market-selections, additionally drop events with an out-of-order data.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"