Webhooks (push events)

If you don't want to poll for state changes, register a webhook receiver. ProphetX pushes async events — contract updates, contract settlements, market-order state changes, and parlay state 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.

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" } — fires a test message and reports round-trip latency.

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/parlaysParlayEvent — parlay created / updated / deleted.

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 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).


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 four 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.
  • 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-staging-api.betprophet.co/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
for t in contract contract_settlement market_order parlay; 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"