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}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. (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
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 the price returned will effectively be nothing (no offer). 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 with nooffereven 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.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.).
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.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.
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 20 days ago
