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
| Field | Values |
|---|---|
fillStatus | pending (submitted to SPs) → filled | failed |
failReason | A string explanation when fillStatus = failed — often "price moved", meaning you'll need to re-quote. |
settlementStatus | tbd → profit | loss | push | void |
per-leg settlementStatus | profit | 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
POST /private/v1/parlays — create a quoteBody (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.lengthbetween 2 and 12 inclusive.- Each leg requires
eventId,marketId,outcomeId, andcontractId.strikeis only needed for markets that have one (spreads, totals, etc.). quantity > 0in 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 }
]
}
}priceis the combined American price;adjustedPriceis after the ISV fee. Show the adjusted one to your user.maxQuantitycaps how much you can confirm at this price.offermay 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 (badcontractId, missing required field, KYC pending, etc.).
4. POST /private/v1/parlays/{parlayId}/confirm — commit
POST /private/v1/parlays/{parlayId}/confirm — commitBody (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
GET /private/v1/parlays/{parlayId} — the full stateResponse (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
GET /private/v1/parlays — list a user's parlay historyReturns 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
| Name | Type | Default | Notes |
|---|---|---|---|
limit | integer | 25 | 1 ≤ limit ≤ 100. |
token | string | unset | Opaque cursor — pass the paging.token from the previous response. |
orderBy | string | uuid | Field to order by. |
orderDir | enum | desc | asc | desc. |
type | enum | all | confirmed | canceled | all. |
settlementType | enum | all | pending | settled | all. |
dateRangeFilter | enum | all | today | this_week | this_month | all. Filters by createdAt. |
parlayIDs | string | unset | Comma-separated list of parlay IDs to filter to. |
Response (ListParlaysResponse)
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.payoutis 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:
- Pop the user back to the quote screen.
- Call
POST /parlaysagain with the same legs (or let the user edit them). - Show the new
offer.adjustedPriceandoffer.maxQuantity. - 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"Updated about 5 hours ago
