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. matched_order and market_selection cannot be triggered from this endpoint yet — you'll only see those event types on real activity.

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,
    "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 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.

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 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,
    "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 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,
    "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.
  • priceFormatsprice expressed in every supported format. Present only when the ISV has additional price formats enabled.
  • fillPrice — the price this specific fill matched at (may improve on price).
  • fillPriceFormatsfillPrice expressed 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. 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,
            "priceFormats": {
              "american": -110,
              "decimal": 1.9091,
              "cents": 52.38,
              "probability": 52.38
            },
            "quantity": 50,
            "value": 45.45,
            "updatedAt": "..."
          }
        ],
        [
          {
            "outcomeId": 5,
            "name": "Detroit Pistons",
            "contractId": "abc123...",
            "strike": 0,
            "price": +100,
            "priceFormats": {
              "american": 100,
              "decimal": 2.0000,
              "cents": 50.00,
              "probability": 50.00
            },
            "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 nine 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 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.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 (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"

Did this page help you?