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. All three ofPOST /private/v1/parlays,GET /private/v1/parlays/{parlayId}, andPOST /private/v1/parlays/{parlayId}/confirmare gated server-side on theparlayuser permission (via thex-required-permissionannotation on the spec). A user without it gets403 permission_denied. CallGET /private/v1/users/USERID/permissionsfirst to see whether they're cleared — see User Onboarding.
For single-contract orders, see Market Orders (single-contract trades).
1. The flow
GET /private/v1/parlays/supported-contracts ─► pre-filter a bet slip to
parlay-eligible contracts
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. GET /private/v1/parlays/supported-contracts — pre-filter a bet slip (optional)
GET /private/v1/parlays/supported-contracts — pre-filter a bet slip (optional)Not every contract can be priced as a parlay leg. Sports pricing engines (SPs) selectively opt contracts in and out, so if you send POST /parlays with a leg that's currently unsupported, the request will validate but fail to quote (400 no_offers_available) once no SP can price the combo. This endpoint gives you the eligibility list up front so you can grey out unsupportable legs before the user builds a slip that can't be priced.
Auth: user-scoped (sub + subsig). Requires the parlay permission — same gate as the other parlay endpoints.
Query:
| Name | Required | Notes |
|---|---|---|
eventIds | yes | Comma-separated sport event IDs. |
Response (ParlaySupportedContractsResponse):
{
"supportedContracts": [
{
"eventId": 20023797,
"contractIds": [
"8315d4bc44d06eef736959e0b214c170",
"2d179d1578e669e97a2e57735f0cbef8"
]
}
]
}Notes:
- Events with zero supported contracts are omitted from the response entirely — don't rely on every event you asked about appearing in
supportedContracts. If an event isn't in the array, none of its contracts are parlay-eligible right now. - Support can change over time — treat this as a live snapshot, not a static configuration. Re-check before quoting when a lot of time has passed between slip build and confirm.
- This is advisory only: the actual parlay quote via
POST /parlaysis the source of truth. Usesupported-contractsto keep the UX clean, but expectPOST /parlaysto sometimes come back with400 no_offers_availableeven for a contract you last saw as supported.
4. 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.
Failure modes:
422— out-of-range leg count, malformed body, or leg validation (badcontractId, missing required field, etc.).400 no_offers_available— no SP could price the combo. There's no partial/absent-offersuccess response for this case — the request fails outright. Your options are to retry, drop legs, or reduce quantity.
5. 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.422— malformed body.400 quantity_exceeds_offer— requested quantity is above the offer'smaxQuantity.400 parlay_not_valid— the parlay wasn't found or has expired.
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.
6. 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.
7. 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.
8. 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 —
requestedQuantityis deducted, fee is taken (feeon the response,ORDER_FEEtransaction), and the remainingquantityis what actually goes on the parlay. - Settlement — when every leg has resolved, ProphetX credits the payout on a win (
PAYtransaction,payoutset,settlementStatus = profit) or settles toloss/push/voidwith the corresponding ledger entry. TheParlayEventwebhook fires on every state change.
No manual posting from your side. See Wallets for the resulting TransactionType rows.
9. 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.
10. Curl
BASE="https://isv-api.sandbox.prophetx.dev/private/v1"
# 0. (Optional) Pre-filter: which contracts on these events can be parlayed?
curl "$BASE/parlays/supported-contracts?eventIds=1,2" \
-H "Authorization: Bearer $JWT_USER"
# 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 15 days ago
