Parlays (2–12 leg trades)

A parlay combines 2–12 individual selections into one order: every leg must settle as profit for the parlay to settle as profit. The workflow is quote → confirm → poll, not a single-shot submit, because the price is stitched together from the constituent legs and can move while your user is deciding.

Auth: user-scoped on all three endpoints (sub == acting user's UUID + subsig). See Authentication.

Required permission: parlay. GET /private/v1/parlays/{parlayId} and POST /private/v1/parlays/{parlayId}/confirm are gated server-side on the parlay 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. (Quote creation is not gated by this permission — quoting is allowed regardless; the gate applies when the user attempts to commit or read their own parlay.)

For single-contract orders, see Market Orders (single-contract trades).


1. The flow

POST /private/v1/parlays                       ─►  201 with parlayId + offer
POST /private/v1/parlays/{parlayId}/confirm    ─►  commit at the quoted price
GET  /private/v1/parlays/{parlayId}            ─►  poll fillStatus + settlementStatus
GET  /private/v1/parlays                       ─►  user's parlay history (token-paginated)

Polling is the simplest option. If you'd rather not poll, subscribe to ParlayEvent webhooks — see Webhooks (push events).


2. The state model

FieldValues
fillStatuspending (submitted to SPs) → filled | failed
failReasonA string explanation when fillStatus = failed — often "price moved", meaning you'll need to re-quote.
settlementStatustbdprofit | loss | push | void
per-leg settlementStatusprofit | loss | tbd | push | void

The most common failure path is: you got a quote, the user took a moment, the SP price moved, and confirm now fails. The recovery is to re-quote with the same legs and try again.


3. POST /private/v1/parlays — create a quote

Body (CreateParlayRequest):

{
  "quantity": 10.00,
  "legs": [
    { "eventId": 1, "marketId": 7, "outcomeId": 2, "contractId": "abc..." },
    { "eventId": 2, "marketId": 9, "outcomeId": 1, "contractId": "def...", "strike": 220.5 }
  ]
}

Rules:

  • legs.length between 2 and 12 inclusive.
  • Each leg requires eventId, marketId, outcomeId, and contractId. strike is only needed for markets that have one (spreads, totals, etc.).
  • quantity > 0 in USD.

Response (201, CreateParlayResponse):

{
  "parlayId": "00000000-0000-0000-0000-000000000000",
  "offer": {
    "price": 615,
    "adjustedPrice": 590,
    "maxQuantity": 50.00,
    "legs": [
      { "contractId": "abc...", "price": -120 },
      { "contractId": "def...", "price": +150 }
    ]
  }
}
  • price is the combined American price; adjustedPrice is after the ISV fee. Show the adjusted one to your user.
  • maxQuantity caps how much you can confirm at this price.
  • offer may be absent — that means no SP could price the combo. You can't confirm; the options are to retry, drop legs, or reduce quantity.

Failure modes:

  • 400 — out-of-range leg count, malformed body.
  • 422 — leg validation (bad contractId, missing required field, KYC pending, etc.).

4. POST /private/v1/parlays/{parlayId}/confirm — commit

Body (ConfirmParlayRequest):

{ "quantity": 10.00 }

quantity > 0, and must be ≤ the offer.maxQuantity returned by the quote.

Response (200, ConfirmParlayResponse):

{
  "parlayId": "...",
  "confirmedAt": "...",
  "requestedQuantity": 10.00,
  "fee": 0.50,
  "quantity": 9.50
}

Failure modes:

  • 402 — insufficient balance.
  • 400 — bad body.
  • 422 — validation failure (e.g. quantity exceeds offer).

After confirm, the parlay is submitted to SPs for filling. Move on to polling — or subscribe to the parlay webhook and let updates push to you.


5. GET /private/v1/parlays/{parlayId} — the full state

Response (Parlay):

{
  "parlayId": "...",
  "fillStatus": "filled",
  "failReason": null,
  "settlementStatus": "tbd",
  "quotedPrice": -115.0,
  "adjustedPrice": -120.0,
  "requestedQuantity": 50.00,
  "fee": 2.00,
  "quantity": 48.00,
  "filledQuantity": 48.00,
  "unfilledQuantity": 0.00,
  "payout": null,
  "confirmedAt": "...",
  "legs": [
    {
      "contractId": "abc...",
      "event": { "...": "..." },
      "market": { "...": "..." },
      "selection": { "...": "..." },
      "score": { "...": "..." },
      "settlementStatus": "tbd"
    }
  ]
}

payout is populated only when settlementStatus = profit.


6. GET /private/v1/parlays — list a user's parlay history

Returns the requesting user's parlays, most-recent first, token-paginated. The response shape is different from the GET-by-ID above — this endpoint returns ParlayHistoryItem (a lighter-weight history record), not the full Parlay.

Query parameters

NameTypeDefaultNotes
limitinteger251 ≤ limit ≤ 100.
tokenstringunsetOpaque cursor — pass the paging.token from the previous response.
orderBystringuuidField to order by.
orderDirenumdescasc | desc.
typeenumallconfirmed | canceled | all.
settlementTypeenumallpending | settled | all.
dateRangeFilterenumalltoday | this_week | this_month | all. Filters by createdAt.
parlayIDsstringunsetComma-separated list of parlay IDs to filter to.

Response (ListParlaysResponse)

{
  "parlays": [
    {
      "parlayId": "00000000-0000-0000-0000-000000000000",
      "status": "filled",
      "settlementStatus": "tbd",
      "requestedPrice": -115.0,
      "requestedQuantity": 50.00,
      "confirmedPrice": -118.0,
      "quantity": 48.00,
      "settledPrice": null,
      "maxProfit": 41.60,
      "payout": null,
      "createdAt": "...",
      "updatedAt": "...",
      "legs": [
        { "contractId": "abc...", "eventId": 1, "marketId": 7, "outcomeId": 2 }
      ]
    }
  ],
  "paging": { "limit": 25, "token": "eyJpZCI6MTAwfQ==" }
}

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

Field meanings

  • status — lifecycle status as a free-form string for forward compatibility. Known values: open, filled, finalized, pending_payout, payout_submitted, settled, rejected, void, failed. Don't switch exhaustively on this; treat unknowns as "no further state changes expected on our side".
  • settlementStatus — same enum as the GET-by-ID: profit | loss | tbd | push | void.
  • requestedPrice / requestedQuantity — the quote you confirmed against.
  • confirmedPrice / quantity — the price actually filled and the quantity after fee, populated once filled (absent before).
  • settledPrice / payout — populated once settled. payout is realized profit.
  • maxProfit — projected profit if the parlay wins fully.

The list endpoint is a thin summary view — there's intentionally less information here than in GET /parlays/{parlayId}. Use the list to populate a history UI, and the GET-by-ID when the user clicks into one row.


7. Money movement is automatic

Just like single-contract orders, you don't move money for parlay flow. ProphetX handles it on its side:

  • Quantity debit at confirm — requestedQuantity is deducted, fee is taken (fee on the response, ORDER_FEE transaction), and the remaining quantity is what actually goes on the parlay.
  • Settlement — when every leg has resolved, ProphetX credits the payout on a win (PAY transaction, payout set, settlementStatus = profit) or settles to loss / push / void with the corresponding ledger entry. The ParlayEvent webhook fires on every state change.

No manual posting from your side. See Wallets for the resulting TransactionType rows.


8. Handling failed fills

If fillStatus = failed after confirm, failReason will usually read "price moved" — meaning the SP pulled or revised their price between your quote and your confirm. Standard handling:

  1. Pop the user back to the quote screen.
  2. Call POST /parlays again with the same legs (or let the user edit them).
  3. Show the new offer.adjustedPrice and offer.maxQuantity.
  4. Confirm again if the user accepts.

Don't try to retry-confirm the same parlayId — it's a terminal state.


9. Curl

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

# 1. Quote
curl -X POST "$BASE/parlays" \
  -H "Authorization: Bearer $JWT_USER" \
  -H "Content-Type: application/json" \
  -d '{
    "quantity": 10.00,
    "legs": [
      { "eventId":1, "marketId":7, "outcomeId":2, "contractId":"abc..." },
      { "eventId":2, "marketId":9, "outcomeId":1, "contractId":"def...", "strike":220.5 }
    ]
  }'
# → 201 { "parlayId":"<id>", "offer": { ... } }

# 2. Confirm
curl -X POST "$BASE/parlays/<id>/confirm" \
  -H "Authorization: Bearer $JWT_USER" \
  -H "Content-Type: application/json" \
  -d '{ "quantity": 10.00 }'

# 3. Poll
curl "$BASE/parlays/<id>" -H "Authorization: Bearer $JWT_USER"

# 4. History (most recent first; paginate via `paging.token`)
curl "$BASE/parlays?limit=25&type=confirmed&dateRangeFilter=this_week" \
  -H "Authorization: Bearer $JWT_USER"