Market Orders (single-contract trades)

A market order is a single-contract order at the best available price. There are three endpoints: estimate (no commit), submit (asynchronous fill), and a state read. The submit returns immediately with a refId, and you either poll or subscribe to webhooks to find out what happens next.

Auth: user-scoped on all three (sub must equal the acting user's UUID, plus subsig). See Authentication.

Required permission: market-order. All four endpoints below are gated server-side on the market-order user permission (via the x-required-permission annotation on the spec). A user without it gets 403 permission_denied. Call GET /private/v1/users/USERID/permissions first to see whether they're cleared — see User Onboarding.

For multi-leg orders, see Parlays (2–12 leg trades) instead.


1. The flow

POST /private/v1/market-orders/estimate    ─►  see expected price + fee + available qty
POST /private/v1/market-orders             ─►  202 with refId
GET  /private/v1/market-orders/{refId}     ─►  poll status, fill progress, P/L
GET  /private/v1/market-orders             ─►  the caller's order history (token-paginated)

Polling is the simplest option. For lower-latency updates without polling, subscribe to MarketOrderEvent webhooks — see Webhooks (push events).


2. The state model

FieldValues
statuspendingcompleted | canceled | failed
fillStatusopenpartialfilled
winningStatustbdprofit | loss | push (after the event settles)

Small naming inconsistency to be aware of: in webhook payloads, fillStatus uses resting instead of open for the same state. See Webhooks (push events). Treat them as synonyms.


3. POST /private/v1/market-orders/estimate

A no-commit quote. Use this to show the user what they'd get before they confirm.

Body (EstimateMarketOrderRequest):

{ "contractId": "183c6ffeb2b4b6772a5afb6336293240", "quantity": 25.00 }
  • quantity >= 0 allowed here. Use 0 to see the best available price without specifying a size.

Response (EstimateMarketOrderResponse):

{
  "availableQuantity": 50.00,
  "expectedAveragePrice": -110,
  "expectedAveragePriceAdjusted": -115,
  "expectedPayout": 45.45,
  "expectedPayoutAdjusted": 43.10,
  "expectedFeeAmount": 1.50,
  "maxQuantity": 200.00,
  "priceList": [-110, -109, -108]
}

Field meanings:

  • availableQuantity — fillable at the expected average price.
  • maxQuantity — total liquidity across all price levels. Taking the whole thing would mean a worse blended price.
  • Prices are in American format (-110, +150).
  • The *Adjusted variants include the ISV fee.
  • priceList — the price levels visible at estimate time. Echo both expectedAveragePrice and priceList back into the submit request — they're a required guardrail (see below).

The common failures are 422 with INVALID_CONTRACT (the contract isn't valid or has gone stale) or with an insufficient-liquidity error.


4. POST /private/v1/market-orders

Submit the order. Asynchronous — the server returns 202 immediately with a reference ID, and the actual fill happens in the background.

Body (SubmitMarketOrderRequest):

{
  "contractId": "183c6ffeb2b4b6772a5afb6336293240",
  "quantity": 25.00,
  "expectedAveragePrice": -110,
  "priceList": [-110, -109, -108]
}
  • quantity > 0 required (strict — 0 is allowed for estimate but not for submit).
  • expectedAveragePrice and priceList are required guardrails. Pass back exactly what you got from the prior /estimate response. If the live price has moved off the listed levels, the backend rejects the submit so you can re-estimate before committing the user's funds. There's no way to bypass this — the only valid value is what came out of the estimate.

Response (202, MarketOrderResponse): same shape as the GET below, with initial status: "pending", fillStatus: "open", filledQuantity: 0.

The refId is not a UUID — it's a string, possibly prefixed with isv_. Treat it as opaque.

Common failure modes:

  • 422 INVALID_CONTRACT — unknown or expired contract.
  • 422 user_pending_kyc — the user hasn't passed KYC yet.
  • 422 — the live price has moved off the levels in priceList. Re-call /estimate to get fresh expectedAveragePrice + priceList, show the new quote to the user, then resubmit.

Insufficient balance is NOT a submit-time error. If the user's wallet balance is less than the requested quantity, the API still returns 200 with status: "pending" — and then the matching engine cancels the order almost immediately, flipping it to status: "canceled" / filledQuantity: 0. There is no 402 here. Poll GET /market-orders/{refId} (or wait for the MarketOrderEvent webhook) and confirm status != "pending" before reporting success to the user. A canceled status with filledQuantity: 0 shortly after submit is the signal to re-check the user's balance.


5. GET /private/v1/market-orders/{refId}

QueryDefaultEffect
fillsfalseSet to true to include a per-fill breakdown in response.fills. Omitted by default to keep the payload small.

Response (MarketOrderResponse):

{
  "refId": "isv_xyz",
  "contractId": "183c6ffeb2b4b6772a5afb6336293240",
  "requestedQuantity": 25.00,
  "fee": 0.50,
  "quantity": 24.50,
  "filledQuantity": 24.50,
  "openQuantity": 0,
  "expectedAveragePrice": -110,
  "currentAveragePrice": -108,
  "status": "completed",
  "fillStatus": "filled",
  "winningStatus": "tbd",
  "eventId": 999999,
  "marketId": 219,
  "strike": 2.5,
  "createdAt": "...",
  "updatedAt": "...",
  "fills": null
}

Read these in pairs:

  • requestedQuantity is what you asked for; fee is what was taken; quantity = requestedQuantity - fee is what actually went on the order.
  • filledQuantity + openQuantity = quantity.
  • expectedAveragePrice is the floor at submission; currentAveragePrice is the actual achieved price so far.
  • eventId, marketId, and strike are sourced from the matched wagers — they're null until a wager has been spawned (i.e. before any fill activity), and strike is also null for markets that don't carry one (moneyline). Use them for downstream reconciliation if you need event/market context without re-fetching from the fixtures API.
  • fills is null by default. Pass ?fills=true to have the backend populate it with a per-fill breakdown (below).

Per-fill breakdown (?fills=true)

Each MarketOrderFill entry represents one discrete match against the order book — useful when you need to reconcile against a per-trade ledger rather than just the rolled-up order state.

{
  "refId": "isv_xyz",
  "...": "...",
  "fills": [
    {
      "refId": "isv_xyz.f1",
      "quantity": 10.00,
      "filledQuantity": 10.00,
      "openQuantity": 0,
      "averagePrice": -110,
      "fillStatus": "filled",
      "winningStatus": "tbd",
      "eventId": 999999,
      "marketId": 219,
      "strike": 2.5,
      "createdAt": "...",
      "updatedAt": "..."
    },
    {
      "refId": "isv_xyz.f2",
      "quantity": 14.50,
      "filledQuantity": 14.50,
      "openQuantity": 0,
      "averagePrice": -107,
      "fillStatus": "filled",
      "winningStatus": "tbd",
      "createdAt": "..."
    }
  ]
}

Field meanings:

FieldPresenceNotes
refIdalwaysDistinct from the parent refId — treat as opaque.
quantityalwaysQuantity of this fill (after fee, i.e. the size that actually matched).
filledQuantity / openQuantityalwaysPer-fill accounting. On a filled entry, openQuantity is 0.
createdAtalwaysWhen this fill occurred.
averagePriceoptionalAmerican price the fill matched at. Absent while pending.
fillStatusoptionalfilled | open | partial. Per-fill state, distinct from the parent order's fillStatus.
winningStatusoptionalprofit | loss | push | tbd. Set after settlement.
eventId, marketId, strike, updatedAtoptionalSame semantics as on the parent order — sourced from the matched wager. strike is absent for moneyline markets.

fills is null (not []) when ?fills=true is omitted. When ?fills=true is passed and the order has not matched yet, expect fills: [].


6. GET /private/v1/market-orders — list the caller's orders

Returns the caller's market-order history, most-recent first, token-paginated. Each item is the same MarketOrderResponse shape as the GET-by-refId above — no lighter summary view.

Query parameters

NameTypeDefaultNotes
limitinteger251 ≤ limit ≤ 100.
tokenstringunsetOpaque cursor — pass the paging.token from the previous response.
orderBystringcreatedAtField to order by.
orderDirenumdescasc | desc.
statusstringunsetComma-separated MarketOrderStatus values to filter by — any of pending, completed, canceled, failed.
fillStatusstringunsetComma-separated FillStatus values to filter by — any of filled, partial, open.
dateRangeFilterenumalltoday | this_week | this_month | all. Filters by createdAt.

Response (ListMarketOrdersResponse)

{
  "marketOrders": [ { "refId": "isv_xyz", "...": "..." } ],
  "paging": { "limit": 25, "token": "eyJpZCI6MTAwfQ==" }
}

paging.token is absent when there are no more pages — that's how you know to stop.

Failure modes:

  • 401 — JWT missing/invalid.
  • 500 / 503 — server error / service temporarily unavailable.

7. Money movement is automatic

You don't move money for order flow. ProphetX handles it on its side:

  • Order debit at submit — the order's quantity is deducted from the wallet atomically.
  • ISV fee — deducted automatically and surfaced as fee on the order plus an ORDER_FEE transaction in /transactions. You don't compute or post the fee yourself.
  • Settlement — when the contract settles, payouts (PAY), refunds (PUSH / VOID), and the resulting balance change are credited to the wallet automatically. The winningStatus on the order flips and a ContractSettlementEvent fires for the contract.

See Wallets for the resulting TransactionType rows.


8. Polling guidance

A few notes to save you time:

  • Don't tight-loop. A 1–2 second interval during active fill and longer once status stabilizes is plenty.
  • If you'd rather not poll at all, subscribe to the /push/market-orders webhook and let ProphetX push updates. Each event is wrapped in { id, op, timestamp, data } — see Webhooks (push events).
  • A terminal status (completed / canceled / failed) means no further fills. winningStatus may still flip later when the event settles — that comes through the ContractSettlementEvent webhook.

9. Curl

BASE="https://isv-api.sandbox.prophetx.dev/private/v1"

# 1. Estimate — grab expectedAveragePrice + priceList from this response
curl -X POST "$BASE/market-orders/estimate" \
  -H "Authorization: Bearer $JWT_USER" \
  -H "Content-Type: application/json" \
  -d '{ "contractId":"abc...", "quantity":25 }'

# 2. Submit — echo expectedAveragePrice + priceList back from the estimate
curl -X POST "$BASE/market-orders" \
  -H "Authorization: Bearer $JWT_USER" \
  -H "Content-Type: application/json" \
  -d '{
    "contractId":"abc...",
    "quantity":25,
    "expectedAveragePrice":-110,
    "priceList":[-110,-109,-108]
  }'
# → 202 { "refId":"isv_xyz", "status":"pending", ... }

# 3. Poll (or wait for the webhook)
curl "$BASE/market-orders/isv_xyz" -H "Authorization: Bearer $JWT_USER"

# 3b. Poll with per-fill breakdown
curl "$BASE/market-orders/isv_xyz?fills=true" -H "Authorization: Bearer $JWT_USER"

# 4. History (most recent first; paginate via paging.token)
curl "$BASE/market-orders?limit=25&status=pending,completed&dateRangeFilter=this_week" \
  -H "Authorization: Bearer $JWT_USER"


Did this page help you?