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.

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


8. Curl

BASE="https://isv-staging-api.betprophet.co/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"