{"openapi": "3.1.0", "info": {"title": "MagicMarkets API", "version": "0.1.0", "description": "REST and WebSocket API for MagicMarkets. Place orders, stream real-time\nprices, and manage your positions programmatically.\n\n## Authentication\n\nEvery request must be authenticated with an **API key**. Pass it in the\n`X-Api-Key` header:\n\n```\nX-Api-Key: <your-api-key>\n```\n\n### Getting an API key\n\nAPI keys are created and managed through the MagicMarkets website at\n[magicmarkets.com](https://magicmarkets.com/):\n\n1. Go to **Settings -> API**.\n2. Click **Add API Key** and give it a name.\n3. Copy the key value - it is shown **once at creation**, so store it\n   somewhere safe immediately.\n\nYou can also revoke or rename existing keys from the same page. There is\nno endpoint in this API to manage keys - all key management happens on\nthe website.\n\n### Using a key\n\nSend the key on every request:\n\n```bash\ncurl https://magicmarkets.com/v2/xrates/ \\\n  -H \"X-Api-Key: $MAGIC_API_KEY\"\n```\n\n## Quickstart\n\nThis walkthrough goes from zero to a placed bet: stream prices over the\nWebSocket, read a `bet_type` off the feed, quote it as a betslip, place\nan order, and watch the order update on the same socket.\n\n### Before you start\n\nYou need an API key (see **Authentication** above) and Python 3.9+ with\ntwo libraries:\n\n```bash\npip install requests websockets\n```\n\nEvery snippet reads its configuration from the environment:\n\n```bash\nexport MAGIC_API_URL=\"https://<host>/v2\"\nexport MAGIC_WS_URL=\"wss://<host>/v2/stream\"\nexport MAGIC_API_KEY=\"<your-api-key>\"\n```\n\nThree concepts carry the whole flow:\n\n- **Two steps to a bet.** A *betslip* registers your interest in one\n  selection (`POST /v2/betslips/`); its live quote then arrives as\n  `[\"pmm\", ...]` entries on the WebSocket. An *order* commits a stake\n  against that quote (`POST /v2/orders/`). You always create the\n  betslip first.\n- **`bet_type` comes from the feed.** Offers on the WebSocket carry the\n  `bet_type` string ready to use - pass it to `POST /v2/betslips/`\n  verbatim. You never need to construct or parse it (the grammar in\n  **Sports & bet types** is reference material, not required reading).\n- **Stakes are USDT.** Wherever money appears it is a\n  `[\"USDT\", amount]` pair.\n\n### Step 1 - Check your key\n\nBefore opening the socket, prove the key works with a cheap REST call -\na bad key fails the WebSocket upgrade with HTTP 401, so verify the key\nfirst:\n\n```bash\ncurl \"$MAGIC_API_URL/xrates/\" -H \"X-Api-Key: $MAGIC_API_KEY\"\n```\n\nA `200` with `{\"status\": \"ok\", ...}` means you are good to go.\n\n### Step 2 - Connect to the stream\n\nConnect with the key as a query parameter. Every frame the server sends\nis a batch envelope `{\"ts\": ..., \"data\": [...]}` - iterate `data[]` and\ndispatch on each entry's leading type tag (see the **Streaming API**\nendpoint for the full wire format):\n\n```python\nimport json, os\nfrom websockets.sync.client import connect\n\nws = connect(f\"{os.environ['MAGIC_WS_URL']}?api_key={os.environ['MAGIC_API_KEY']}\")\n\nevents, synced = [], False\nwhile not synced:\n    frame = json.loads(ws.recv())\n    for entry in frame[\"data\"]:\n        if entry[0] == \"event\":\n            events.append(entry[1])   # {\"sport\": ..., \"event_id\": ..., ...}\n        elif entry[0] == \"sync\":\n            synced = True\n```\n\nAfter `[\"sync\", ...]` you hold the list of currently-priced events, e.g.:\n\n```json\n[\"event\", {\"event_type\": \"normal\", \"sport\": \"fb\",\n  \"event_id\": \"2026-06-15,1001,2002\", \"competition_id\": 1,\n  \"competition_name\": \"England Premier League\",\n  \"competition_country\": \"XE\", \"home\": \"Arsenal\", \"away\": \"Chelsea\",\n  \"event_name\": \"Arsenal vs. Chelsea\", \"ir_status\": \"pre_event\",\n  \"start_time\": \"2026-06-15T15:00:00Z\"}]\n```\n\n### Step 3 - Register for offers on an event\n\nPick an event and register. The server replies with one `[\"offer\", ...]`\nper bet type (the snapshot), then an ok `[\"response\", ...]`:\n\n```python\nevent = events[0]\nws.send(json.dumps([\"register_event\", event[\"sport\"], event[\"event_id\"]]))\n\noffers, registered = [], False\nwhile not registered:\n    frame = json.loads(ws.recv())\n    for entry in frame[\"data\"]:\n        if entry[0] == \"offer\":\n            offers.append(entry[1])\n        elif entry[0] == \"response\":\n            if entry[1][\"status\"] == \"ok\":\n                registered = True\n            else:\n                raise SystemExit(f\"register_event failed: {entry[1]['code']}\")\n```\n\n(Registering an event that has not yet appeared in the sync stream is\nnot an error - you just get an empty snapshot, and offers start flowing\nif the event becomes priced. Prefer event ids you saw in the sync\nstream.) From now on the full offer set is re-broadcast whenever this\nevent's prices change.\n\n### Step 4 - Read the bet_type off an offer\n\nEach offer is one priced selection. Everything the next step needs is\nalready in it:\n\n```json\n[\"offer\", {\n  \"sport\": \"fb\",\n  \"event_id\": \"2026-06-15,1001,2002\",\n  \"bet_type\": \"for,ah,h,1\",\n  \"market_type\": \"ah\",\n  \"in_running\": false,\n  \"price_list\": [\n    {\"effective\": {\"price\": 2.0, \"min\": [\"USDT\", 5.0], \"max\": [\"USDT\", 150.0]}},\n    {\"effective\": {\"price\": 1.99, \"min\": null, \"max\": [\"USDT\", 80.0]}}\n  ]\n}]\n```\n\n`price_list` is sorted by price descending; `min` is `null` when there\nis no minimum stake. Pick a priced offer - its `sport`, `event_id` and\n`bet_type` are everything the next step needs:\n\n```python\noffer = next(o for o in offers if o[\"price_list\"])\n```\n\n### Step 5 - Create a betslip\n\nQuote the selection by passing the offer's fields through verbatim:\n\n```python\nimport requests\n\nAPI = os.environ[\"MAGIC_API_URL\"]\nHEADERS = {\"X-Api-Key\": os.environ[\"MAGIC_API_KEY\"]}\n\nbetslip = requests.post(f\"{API}/betslips/\", headers=HEADERS, json={\n    \"sport\": offer[\"sport\"],\n    \"event_id\": offer[\"event_id\"],\n    \"bet_type\": offer[\"bet_type\"],\n    \"betslip_type\": \"normal\",\n}).json()[\"data\"]\n```\n\nThe response registers the betslip - note the `betslip_id` and the\n`expiry_ts` (betslips are short-lived; re-create one that expires).\nDo not expect prices in this response: your private quote arrives on\nthe WebSocket you already hold open, as `[\"pmm\", ...]` entries carrying\nyour `betslip_id` - typically within a couple of seconds, refreshed\nwhile the betslip stays open:\n\n```python\nquote = None\nwhile quote is None:\n    frame = json.loads(ws.recv())\n    for entry in frame[\"data\"]:\n        if entry[0] == \"pmm\" and entry[1][\"betslip_id\"] == betslip[\"betslip_id\"]:\n            if entry[1][\"price_list\"]:\n                quote = entry[1]\n```\n\n```json\n[\"pmm\", {\n  \"betslip_id\": \"65b6ff7da480479b9dda1c7ff765c434\",\n  \"sport\": \"fb\",\n  \"event_id\": \"2026-06-15,1001,2002\",\n  \"bet_type\": \"for,ah,h,1\",\n  \"status\": {\"code\": \"success\"},\n  \"price_list\": [\n    {\"effective\": {\"price\": 2.0, \"min\": [\"USDT\", 5.0], \"max\": [\"USDT\", 150.0]}}\n  ],\n  \"total\": [\"USDT\", 150.0]\n}]\n```\n\nThe `price_list` uses the same format as offers - prices descending,\nstakes in USDT. A pmm whose `price_list` stays empty means there is no\nliquidity for this selection right now - pick another offer and\nre-quote.\n\nIf you are not holding the stream open, poll\n`GET /v2/betslips/{betslip_id}/` until `price_list` populates.\n\n### Step 6 - Place an order\n\nCommit a stake at one of the quoted prices. Three fields are\nrequired (`betslip_id`, `price`, `stake`); `duration` is the order's\nlifetime in **seconds** (optional, default 15):\n\n```python\nbest = quote[\"price_list\"][0][\"effective\"]\n\norder = requests.post(f\"{API}/orders/\", headers=HEADERS, json={\n    \"betslip_id\": betslip[\"betslip_id\"],\n    \"price\": best[\"price\"],\n    \"stake\": [\"USDT\", 10.0],\n    \"duration\": 5.0,\n}).json()[\"data\"]\n```\n\nThe response confirms acceptance - `order_id` and `status: \"open\"`\n(abridged; `bets` appear on the subsequent updates as the order fills):\n\n```json\n{\n  \"order_id\": 5001,\n  \"status\": \"open\",\n  \"bet_type\": \"for,ah,h,1\",\n  \"sport\": \"fb\",\n  \"want_price\": 2.0,\n  \"want_stake\": [\"USDT\", 10.0],\n  \"closed\": false,\n  \"price\": null,\n  \"stake\": null,\n  \"profit_loss\": null\n}\n```\n\n### Step 7 - Watch the order on the WebSocket\n\nOrder updates arrive on the socket you already hold open, as\n`[\"order\", ...]` and `[\"bet\", ...]` entries in the same envelopes as\noffers. An order moves `open -> pending -> done | failed`; when it\ncloses, `price`, `stake` and `profit_loss` are filled in:\n\n```python\nwhile True:\n    frame = json.loads(ws.recv())\n    for entry in frame[\"data\"]:\n        if entry[0] == \"order\" and entry[1][\"order_id\"] == order[\"order_id\"]:\n            o = entry[1]\n            print(\"order:\", o[\"status\"], o.get(\"close_reason\"))\n            if o[\"status\"] in (\"done\", \"failed\"):\n                raise SystemExit(0)\n```\n\nNote that a `done` order is filled, not settled - the final\n`profit_loss` lands after the event finishes. To re-check an order\nlater (e.g. after a restart), `GET /v2/orders/{order_id}/` returns the\nsame object on demand.\n\n### Handling errors\n\nAlways check `status` before reading `data`. REST errors use the\nenvelope from the **Errors** section below - `validation_error`\nbodies name the offending field (e.g. `bet_type: [\"invalid_bet_type\"]`),\nand on `429` honour `data.retry_after` (the limits are listed under\n**Rate limiting**). WebSocket errors arrive\nin-band as `[\"response\", {\"status\": \"error\", ...}]` entries; the reason table and the\nsilent-close cases are documented on the **Streaming API** endpoint.\n\n### Complete example\n\nThe whole flow, runnable as-is with the three environment variables set:\n\n```python\nimport json, os\nimport requests\nfrom websockets.sync.client import connect\n\nAPI = os.environ[\"MAGIC_API_URL\"]\nKEY = os.environ[\"MAGIC_API_KEY\"]\nHEADERS = {\"X-Api-Key\": KEY}\n\n# 1. verify the key via REST first - the socket closes silently on a bad key\nrequests.get(f\"{API}/xrates/\", headers=HEADERS).raise_for_status()\n\nwith connect(f\"{os.environ['MAGIC_WS_URL']}?api_key={KEY}\") as ws:\n    # 2. initial sync: collect events until [\"sync\", ...]\n    events, synced = [], False\n    while not synced:\n        frame = json.loads(ws.recv())\n        for entry in frame[\"data\"]:\n            if entry[0] == \"event\":\n                events.append(entry[1])\n            elif entry[0] == \"sync\":\n                synced = True\n    if not events:\n        raise SystemExit(\"no priced events right now\")\n\n    # 3+4. register events until one returns a priced offer\n    offer = None\n    for event in events:\n        ws.send(json.dumps([\"register_event\", event[\"sport\"], event[\"event_id\"]]))\n        offers, registered = [], False\n        while not registered:\n            frame = json.loads(ws.recv())\n            for entry in frame[\"data\"]:\n                if entry[0] == \"offer\":\n                    offers.append(entry[1])\n                elif entry[0] == \"response\":\n                    if entry[1][\"status\"] == \"ok\":\n                        registered = True\n                    else:\n                        raise SystemExit(f\"register_event failed: {entry[1]['code']}\")\n        offer = next((o for o in offers if o[\"price_list\"]), None)\n        if offer:\n            print(\"picked\", offer[\"bet_type\"], \"on\", event[\"event_name\"])\n            break\n        ws.send(json.dumps([\"unregister_event\", event[\"sport\"], event[\"event_id\"]]))\n    if offer is None:\n        raise SystemExit(\"no priced offers right now\")\n\n    # 5. register a betslip (bet_type verbatim), then read the quote off the socket\n    resp = requests.post(f\"{API}/betslips/\", headers=HEADERS, json={\n        \"sport\": offer[\"sport\"],\n        \"event_id\": offer[\"event_id\"],\n        \"bet_type\": offer[\"bet_type\"],\n        \"betslip_type\": \"normal\",\n    }).json()\n    if resp[\"status\"] != \"ok\":\n        raise SystemExit(f\"betslip rejected: {resp}\")\n    betslip = resp[\"data\"]\n\n    quote = None\n    while quote is None:\n        frame = json.loads(ws.recv())\n        for entry in frame[\"data\"]:\n            if entry[0] == \"pmm\" and entry[1][\"betslip_id\"] == betslip[\"betslip_id\"]:\n                if entry[1][\"price_list\"]:\n                    quote = entry[1]\n    print(\"quoted\", quote[\"price_list\"][0])\n\n    # 6. place an order at the quote's best price\n    best = quote[\"price_list\"][0][\"effective\"]\n    resp = requests.post(f\"{API}/orders/\", headers=HEADERS, json={\n        \"betslip_id\": betslip[\"betslip_id\"],\n        \"price\": best[\"price\"],\n        \"stake\": [\"USDT\", 10.0],\n        \"duration\": 5.0,\n    }).json()\n    if resp[\"status\"] != \"ok\":\n        raise SystemExit(f\"order rejected: {resp}\")\n    order = resp[\"data\"]\n    print(\"placed order\", order[\"order_id\"], \"status:\", order[\"status\"])\n\n    # 7. watch it on the same socket\n    while True:\n        frame = json.loads(ws.recv())\n        for entry in frame[\"data\"]:\n            if entry[0] == \"order\" and entry[1][\"order_id\"] == order[\"order_id\"]:\n                o = entry[1]\n                print(\"order:\", o[\"status\"], o.get(\"close_reason\"))\n                if o[\"status\"] in (\"done\", \"failed\"):\n                    raise SystemExit(0)\n```\n\nFrom here: the **Streaming API** endpoint documents every message on the\nsocket, **Betslips** and **Orders** cover the remaining endpoints\n(including parlays and lay orders), and **Heartbeats** provides a\ndead-man's switch for automated trading.\n\n## Response format\n\nAll JSON responses share a common envelope:\n\n```json\n{ \"status\": \"ok\", \"data\": ... }\n```\n\nOn error:\n\n```json\n{ \"status\": \"error\", \"code\": \"<code>\", \"data\": <details> }\n```\n\n`data` may be `null`, a string, or an object - depending on the code. See\nthe **Errors** section below for the most common shapes.\n\n## Errors\n\nError responses always include `status: \"error\"` and a stable string\n`code` that clients should branch on. The HTTP status conveys the\ncategory; `code` narrows it down.\n\n### Common codes\n\n| HTTP | `code` | When |\n|------|--------|------|\n| 400 | `validation_error` | Request body or query failed validation. `data.validation_errors` is a `{ field: [reason, ...] }` map; cross-field problems land in `non_field_errors`. A rejected list field is index-keyed instead: `{ field: { \"0\": [reason, ...] } }`. |\n| 400 | `order_closed` | `POST /v2/orders/{id}/close/` on an order that exists but is already closed or settled. Distinct from `not_found`, which means the order id is unknown. |\n| 401 | `auth_error` | API key missing, malformed, or rejected. Also used by login flows for 2FA / inactive / locked accounts. |\n| 403 | `forbidden` | The key is valid but is not allowed to perform this action. |\n| 403 | `invalid_customer` | Market data: the feed has no record of your account. Contact support; a retry gets the same answer. |\n| 404 | `not_found` | The addressed resource (betslip, order, heartbeat, token, session) does not exist or is not visible to this key. |\n| 409 | `order_already_created` | A `request_uuid` from `POST /v2/orders/` was reused. `data` includes the existing `order_id`. |\n| 409 | `limit_reached` | A per-customer cap was hit (e.g. maximum API tokens). `data.detail` describes the cap. |\n| 429 | `throttled` | Rate limit hit - see **Rate limiting** below. `data` is `{ \"message\": \"...\", \"retry_after\": <seconds> }` and a `Retry-After` header is sent. |\n| 500 | `server_error` | Unexpected internal error. `data` is `[\"An error has occurred, token:\", \"<token>\"]`; quote the token if you contact support. |\n| 503 | `warming` or no envelope | `warming` (market data): the feed is loading its market data or customer records, which clears by itself, so retry with backoff. No envelope (any endpoint): an upstream service could not be reached, and the body is `{ \"detail\": \"Service unavailable\" }`; retry with backoff. |\n\nFor `validation_error` responses, branch on the inner reason - the\nkeys of `data.validation_errors` (`non_field_errors` for cross-field\nrejections, otherwise the offending field name). Each endpoint\ndocuments the concrete codes it emits next to its 400 response.\n\n## Rate limiting\n\nLimits are per **account**: all of an account's API keys draw from\none budget. Each limit is a token bucket that refills continuously\nat its own rate, so capacity returns steadily rather than in one\nreset, and an account that has been idle can spend a full bucket in\na single burst.\n\n| Applies to | Limit |\n|------------|-------|\n| All endpoints | 100 requests/second burst, 1200 requests/minute sustained |\n| `POST /v2/betslips/` | 10 requests/second, and 2000 betslips/day |\n| `POST /v2/orders/` | 5 requests/second |\n| `GET /v2/offers/` | 4 requests in flight at once |\n\nThe placement rows are dedicated budgets: a `POST /v2/betslips/` or\n`POST /v2/orders/` call counts only against its own limit, not the\ngeneral one. Polling market data therefore never throttles order\nplacement. Every other endpoint, market data included, shares the\ngeneral budget, so a busy poller leaves less of it for other GETs.\n\nBudgets are keyed on the account, not the source address, so\nspreading an integration across several hosts does not multiply it.\n\nA rejected request returns a `429` `throttled` error with the wait\nin `Retry-After` and `data.retry_after` (integer seconds).\n\nThe `GET /v2/offers/` row counts open requests: an account can have 4\nof them in flight at once, because each one prices every event it names.\nA call can name up to 20 events at the same cost against the cap, so one\ncall naming 20 events beats 20 calls naming one. A request over the cap\ngets the `throttled` envelope. Retry when one of your open requests\nreturns; `retry_after` is a fixed 1 second, because the server cannot\nknow when your open requests will finish.\n\nThe WebSocket stream has no message-rate limit; its connection-level\nlimits (registered-event cap, slow-reader disconnect) are documented\non the **Streaming API** endpoint.\n\nLimits can be adjusted per account - contact support if your\nintegration needs more headroom.\n\n## Currencies\n\nStake fields are `[currency, amount]` tuples. Stakes in responses are\nalways returned in **USDT**:\n\n```json\n[\"USDT\", 115.38]\n```\n\n## Price ticks\n\nSingle-market prices lie on a fixed tick schedule. The tick (the smallest\nstep between two valid prices) widens as the decimal price grows:\n\n| Price (cents) | Decimal price | Tick |\n|---------------|---------------|------|\n| 50c - 99c     | 1.01 - 2      | 0.01 |\n| 33.3c - 50c   | 2 - 3         | 0.02 |\n| 25c - 33.3c   | 3 - 4         | 0.05 |\n| 16.7c - 25c   | 4 - 6         | 0.10 |\n| 10c - 16.7c   | 6 - 10        | 0.20 |\n| 5c - 10c      | 10 - 20       | 0.50 |\n| 3.3c - 5c     | 20 - 30       | 1    |\n| 2c - 3.3c     | 30 - 50       | 2    |\n| 1c - 2c       | 50 - 100      | 5    |\n| 0.1c - 1c     | 100 - 1000    | 10   |\n\nCents are the implied probability of a price: `cents = 100 / decimal\nprice`. The band boundaries are exact in decimal price; the cents labels\nare only approximate, so always match a price to its band by the decimal\nvalue.\n\n### Order prices\n\nThe price you submit on an order is a limit. A bet is taken only at your\nprice or better: better means higher for a back (`for`) order and lower for\na lay (`against`) one.\n\nAn off-tick limit moves to the first tick that honours it, never to one\nthat does not. The market carries only ticks, so that tick is the cheapest\nprice that satisfies the limit you named. A back limit of 7.15 becomes 7.20\nand is not filled at 7.00. The moved price is the one the order runs with,\nand the one the order response reports.\n\n### Feed prices\n\nEvery single-market price delivered on the stream (`price_list`, offers,\npmms) is already on the schedule, so a price quoted straight from the feed\nis always a valid limit.\n\n### Accumulator prices\n\nAn accumulator price is the product of its legs' prices, so it lies on no\ntick schedule. Accumulator prices are quoted, and accepted as limits, at\nfull precision, up to the ceiling of 1000 that every quoted price shares.\n\n## Sports & bet types\n\nEvery betslip, order and bet carries a `sport` and a `bet_type`. They\nare short, opaque-looking strings whose grammar is described here.\n\n### Sport codes\n\n`sport` is a lowercase string. Current values:\n\n| Code | Sport |\n|------|-------|\n| `fb` | Football, full 90 minutes |\n| `fb_ht` | Football, first half only |\n| `fb_2h` | Football, second half only |\n| `fb_et` | Football, extra time only |\n| `fb_corn` | Football corners (90 min) |\n| `fb_corn_ht` | Football corners (1st half) |\n| `fb_book` | Football yellow cards (90 min) |\n| `fb_htft` | Football combined half-time / full-time result |\n| `basket` | Basketball, full match |\n| `basket_ht` | Basketball, first half |\n| `basket_2h` | Basketball, second half |\n| `basket_q1` | Basketball, 1st quarter |\n| `basket_q2` | Basketball, 2nd quarter |\n| `basket_q3` | Basketball, 3rd quarter |\n| `basket_q4` | Basketball, 4th quarter |\n| `tennis` | Tennis |\n| `tt` | Table tennis |\n| `ih` | Ice hockey |\n| `af` | American football |\n| `rl` | Rugby league |\n| `ru` | Rugby union |\n| `arf` | Australian rules football |\n| `hand` | Handball |\n| `volley` | Volleyball |\n| `baseball` | Baseball |\n| `cricket` | Cricket |\n| `darts` | Darts |\n| `snooker` | Snooker |\n| `boxing` | Boxing |\n| `mma` | Mixed martial arts |\n| `golf` | Golf |\n| `cycling` | Cycling |\n| `moto` | Motorsport |\n| `horse` | Horse racing |\n| `dog` | Greyhound racing |\n| `esports` | Esports |\n| `politics` | Political markets |\n| `specials` | Specials / novelty markets |\n\nOn accumulator (parlay) orders and betslips, `sport` is the literal\nstring `parlay` and the per-leg sport sits inside each `legs[]` entry.\n\nTreat the table above as informational, not as a closed enum - new\nsports are added over time. Do not hard-fail on unknown codes.\n\n### Bet type grammar\n\n`bet_type` is a comma-separated string. The first token is the\ndirection:\n\n- `for` - back the outcome (you win if it happens).\n- `against` - lay the outcome (you win if it doesn't).\n\nThe remaining tokens identify the market and its parameters.\n**Handicaps always refer to the home team.**\n\n**Asian handicap lines are integers equal to 4 x the actual line.**\nThis keeps the wire format integer-only across `0.25`-step lines:\n\n| Wire integer | Real line |\n|--------------|-----------|\n| `0`  | 0.0 |\n| `2`  | 0.5 |\n| `7`  | 1.75 |\n| `8`  | 2.0 |\n| `-4` | -1.0 |\n| `-21` | -5.25 |\n\n### Markets\n\nMatch result:\n\n| Bet type | Meaning |\n|----------|---------|\n| `for,h` / `for,d` / `for,a` | Home / Draw / Away win |\n| `for,sd` | Score draw (any non-0-0 draw) |\n| `for,win_90,h` | Home wins in 90 min (excluding extra time) |\n| `for,dnb,h` | Home win, void if draw (draw-no-bet) |\n| `for,hnb,a` | Away win, void if home wins (home-no-bet) |\n| `for,anb,h` | Home win, void if away wins (away-no-bet) |\n| `for,ml,h` | Moneyline - home wins, draw is void |\n| `for,dc,h,d` | Double chance: home or draw |\n| `for,uswin,h` | US-style home win (draw is half-stake split) |\n| `for,awdw,h` | Asian win/draw/win |\n| `for,ko,h` | Home team to kick off |\n| `for,qualify,h` | Home team to qualify |\n\nGoals (totals):\n\n| Bet type | Meaning |\n|----------|---------|\n| `for,over,2.5` / `for,under,2.5` | Over/under non-integer line |\n| `for,overeq,3` / `for,undereq,3` | Over/under integer line, inclusive |\n| `for,exact_total,3` | Exactly 3 goals |\n| `for,exact_total,3,inf` | 3 or more goals |\n| `for,gr,1,3` | Goal range 1-3 inclusive (use `inf` for infinity) |\n| `for,teamgr,h,0,2` | Home team scores 0-2 |\n| `for,odd` / `for,even` | Total goals odd / even |\n| `for,odd,h` / `for,even,a` | Per-team odd / even |\n\nAsian handicaps (lines as 4 x the actual line):\n\n| Bet type | Meaning |\n|----------|---------|\n| `for,ah,h,-4` | Asian handicap, home -1.0 |\n| `for,ahover,7` / `for,ahunder,7` | Asian over/under 1.75 goals |\n| `for,tahover,h,2` / `for,tahunder,a,2` | Team Asian over/under 0.5 goals |\n| `for,eh,h,1` | English handicap, home +1 |\n\nCorrect score and margins:\n\n| Bet type | Meaning |\n|----------|---------|\n| `for,cs,2,1` | Correct score 2-1 |\n| `for,othercs,3,3` | Any score outside `home <= 3 AND away <= 3` |\n| `for,othercs,1,1,3,3` | Any score outside both ranges |\n| `for,wm,h,2,2` | Home wins by exactly 2 |\n| `for,wm,h,2,inf` | Home wins by 2+ |\n| `for,wmo,h,1,2.5` | Home wins by 1 + over 2.5 goals |\n| `for,awm,1` | Absolute margin 1 (either side) |\n| `for,wg,h,2` | Home wins and scores >= 2 |\n| `for,quatro,h,o,2.5` | Home wins AND over 2.5 goals |\n| `for,moou,h,over,2.5` | Match-result + over/under combo |\n| `for,mo_both_score,h,yes` | Home wins AND both teams score |\n| `for,aou,h,3` | Betfair \"any other unquoted\", home, max draw at 3-3 |\n\nScore / clean sheet:\n\n| Bet type | Meaning |\n|----------|---------|\n| `for,score,both,yes` / `for,score,both,no` | Both teams (don't) score |\n| `for,score,either` / `for,score,neither` / `for,score,one` | Score patterns |\n| `for,score,h,yes` / `for,score,h,no` | Home (does not) score |\n| `for,clean,h` | Home clean sheet |\n| `for,clean,both` / `for,clean,either` / `for,clean,neither` / `for,clean,one` | Clean-sheet patterns |\n| `for,fg,no_goal` | No goals (first goal markets) |\n| `for,swm,no_goal` / `for,swm,sd` | Score-and-margin: no goal / score draw |\n\n### Tennis\n\nTennis bet types include a period and a void rule:\n\n```\nfor,tset,<period>,<void_rule>,<unit>[,<market>,<args>...]\n```\n\n- `<period>` - `1`-`5` (a specific set) or `all` (whole match).\n- `<void_rule>` - `vwhole`, `vsetN`, `vgameN` - when the bet voids\n  if a player retires.\n- `<unit>` - `set` or `game`, optionally followed by a market and args.\n\nExamples:\n\n- `for,tset,all,vset1,p1` - player 1 to win the match (voids unless\n  set 1 completes).\n- `for,tset,1,vwhole,p1` - player 1 to win set 1.\n- `for,tset,all,vwhole,game,ahover,62` - total games in the match\n  over 15.5.\n\n### Time-period sports (other than tennis)\n\nBets on a specific period of a match use one of these tokens:\n\n| Token | Meaning |\n|-------|---------|\n| `tp,<period>` | Generic period - `<period>` is `all`, `reg`, or `1`-`9` |\n| `tperiod,<n>` | Specific period (e.g. ice hockey, hand-ball) |\n| `thalf,<n>` | First or second half |\n| `tquarter,<n>` | Quarter (basketball, NFL) |\n| `tinnings,<n>` | Inning (baseball) - `<n>` is integer or `all` |\n| `tmap,<n>` | Map (esports) - `<n>` is `1`-`5` |\n\nThe token is followed by an optional `sub,<subsport>` modifier\n(used for things like darts 180-counts) and then the regular\nmarket and its arguments:\n\n```\nfor,<period_token>[,sub,<subsport>],<market>[,<args>...]\n```\n\nExamples:\n\n- `for,tp,all,ahunder,16` - total under 4.0 across all periods.\n- `for,thalf,1,ah,h,0` - Asian handicap, home 0.0, in the first half.\n- `for,tquarter,2,wdw,h` - home to win the second quarter.\n- `for,tmap,1,ahover,42` - esports, total kills on map 1 over 10.5.\n- `for,tp,all,sub,180,ahover,8` - darts, over 2.0 180s.\n\nThe legacy aliases `tall`, `treg`, `tp1`, `tp2`, ... are no longer\naccepted; use the tokens above.\n\n### Multirunner (outright) events\n\nFor events with many runners (horse racing, golf, etc.):\n\n- `for,win,<team_id>` - runner to win outright.\n- `for,top,<n>,<team_id>` - runner to finish in the top `<n>`\n  (e.g. `for,top,3,1042` means runner 1042 to place top-3).\n\n### Validating a bet type\n\nCall `GET /v2/sports/{sport}/bet_types/{bet_type}/` with the\ncandidate string. A 200 response includes a human-readable\n`bet_type_description` and the win/loss payoff grid; a 400\n(`invalid_bet_type`) means the string did not parse. This endpoint\ncannot validate multirunner bet types (`for,win,...`, `for,top,...`);\nthey need an event context and always return the 400.\n\n## Idempotency\n\n`POST /v2/orders/` accepts an optional `request_uuid`. Retrying the same\nrequest with the same UUID will not create a duplicate order, and the\norder can be retrieved by UUID from `GET /v2/orders/tracked/{uuid}/` for\nseveral days after placement (until the order is purged upstream).\n\n## Machine-readable docs\n\nThe canonical OpenAPI spec is served at:\n\n- `GET /v2/openapi.json` (parsed JSON)\n- `GET /v2/openapi.yaml` (raw YAML)\n\nBoth URLs return the same schema rendered on this page.\n\nFor LLMs and coding agents:\n\n- `GET /llms.txt` - index of machine-readable documentation\n  ([llms.txt](https://llmstxt.org/) format)\n- `GET /docs.md` (alias `GET /llms-full.txt`) - this entire reference as a\n  single Markdown document\n- `GET /docs` with an `Accept: text/markdown` header returns the Markdown\n  reference instead of HTML\n\nAll paths also work behind the `/magic-api` ingress prefix\n(e.g. `/magic-api/llms.txt`, `/magic-api/v2/openapi.yaml`).\n"}, "security": [{"ApiKeyHeader": []}], "tags": [{"name": "Betslips", "description": "Create, retrieve and close betslips. Quotes arrive asynchronously as a single `price_list` (stakes in USDT): poll `GET /v2/betslips/{betslip_id}/` or watch the stream."}, {"name": "Orders", "description": "Place and retrieve orders. All stake fields are converted to USDT. Supports three order formats: normal (match sports), multirunner (outrights), and parlay (accumulators)."}, {"name": "Account", "description": "Account-level information: current balance, open stake, and smart credit, all in USDT."}, {"name": "Heartbeats", "description": "Heartbeat timers that automatically close all open orders when they expire. Use these as a dead-man's switch for automated trading."}, {"name": "Reference", "description": "Exchange rates and bet type information."}, {"name": "Market data", "description": "Event and market discovery over REST. Entries have the same shape as the stream's `event` and `offer` entries. For live prices, use the streaming API: it pushes each change as it happens."}, {"name": "Streaming API", "description": "Real-time WebSocket stream: event discovery, live prices with stakes in USDT (updated every 2 seconds), and account updates (betslips, quotes, orders, bets, balance) on a single socket. Runs as a separate service - connect directly to the stream's WebSocket server."}], "paths": {"/v2/betslips/": {"get": {"tags": ["Betslips"], "summary": "List betslips", "description": "Returns all open betslip IDs for the authenticated customer.", "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "List of betslip IDs", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BetslipListEnvelope"}, "example": {"status": "ok", "data": ["f0a46f72af524d1aa16da56f4e3d23d6", "d796fb4a52f747ce827160af46d60cfd", "56e045c8d62542ecb492e7843ac6b24e"]}}}}}}, "post": {"tags": ["Betslips"], "summary": "Create betslip", "description": "Create a new betslip. For normal/lay bets supply `sport`, `event_id`, and `bet_type`. For parlays supply a `legs` array instead.\n\nThe response carries **no prices**: quotes are gathered asynchronously. Poll `GET /v2/betslips/{betslip_id}/` until `price_list` populates (typically a couple of seconds; watch `expiry_ts`), or read the quote off the stream.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BetslipCreateRequest"}, "examples": {"normal": {"summary": "Normal bet", "value": {"sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "for,h", "betslip_type": "normal", "equivalent_bets": true}}, "lay": {"summary": "Lay bet", "value": {"sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "against,h", "betslip_type": "lay"}}, "parlay": {"summary": "Parlay (accumulator)", "value": {"betslip_type": "parlay", "equivalent_bets": false, "legs": [{"sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "for,h"}, {"sport": "tennis", "event_id": "2026-06-16,501,502", "bet_type": "for,a"}]}}, "exclude_danger": {"summary": "Normal bet with dangerball liquidity excluded", "value": {"sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "for,h", "exclude_danger": true}}}}}}, "responses": {"401": {"$ref": "#/components/responses/Error401"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "400": {"description": "Validation failed", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "examples": {"invalid_bet_type": {"summary": "bet_type did not parse", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"bet_type": ["invalid_bet_type"]}}}}, "event_not_found": {"summary": "Event does not exist", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"non_field_errors": ["event_not_found"]}}}}}}}}, "403": {"description": "Per-customer open-betslip cap exceeded", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "too_many_open_betslips", "data": null}}}}, "201": {"description": "Betslip created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BetslipCreateEnvelope"}, "examples": {"normal": {"summary": "Normal bet", "value": {"status": "ok", "data": {"betslip_id": "bs-spread-002", "sport": "fb", "event_id": "2026-06-15,3003,4004", "bet_type": "for,ahover,7", "bet_type_description": "Over 1.5 (Asian)", "live_score": null, "expiry_ts": 1781234999.0, "is_open": true, "close_reason": null, "equivalent_bets": false, "customer_username": "user1", "customer_ccy": "USDT", "betslip_type": "normal", "user_data": null}}}, "parlay": {"summary": "Parlay - accumulator", "value": {"status": "ok", "data": {"betslip_id": "0e644fdcce7f45c0997427410703efb6", "sport": "parlay", "event_id": "", "bet_type": "parlay", "bet_type_description": "Karlsruher SC AND Draw AND SV Darmstadt 1898 AND Anastasia Potapova", "live_score": null, "expiry_ts": 1775837930.718644, "is_open": true, "close_reason": null, "equivalent_bets": false, "customer_username": "user3", "customer_ccy": "USDT", "betslip_type": "parlay", "user_data": null, "legs": [{"id": 1, "sport": "fb", "event_id": "2026-04-10,224,204", "bet_type": "for,h", "live_score": null, "bet_type_description": "Karlsruher SC", "price": null, "outcome": null}, {"id": 2, "sport": "fb", "event_id": "2026-04-11,1027,211", "bet_type": "for,d", "live_score": null, "bet_type_description": "Draw", "price": null, "outcome": null}, {"id": 3, "sport": "fb", "event_id": "2026-04-11,1080,205", "bet_type": "for,h", "live_score": null, "bet_type_description": "SV Darmstadt 1898", "price": null, "outcome": null}, {"id": 4, "sport": "tennis", "event_id": "2026-04-10,80623,10056707", "bet_type": "for,tset,all,vset1,p1", "live_score": null, "bet_type_description": "Anastasia Potapova", "price": null, "outcome": null}]}}}}}}}}}}, "/v2/betslips/{betslip_id}/": {"get": {"tags": ["Betslips"], "summary": "Get betslip", "description": "Returns a single betslip with prices and stakes in USDT. `price_list` may be empty until quotes arrive, or when nothing is currently quoting the selection.", "parameters": [{"name": "betslip_id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Betslip ID"}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "Betslip not found or not visible to this key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Betslip with prices", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BetslipEnvelope"}, "examples": {"normal": {"summary": "Normal - multiple price levels", "value": {"status": "ok", "data": {"betslip_id": "bs-spread-002", "sport": "fb", "event_id": "2026-06-15,3003,4004", "bet_type": "for,ahover,7", "bet_type_description": "Over 1.5 (Asian)", "live_score": null, "expiry_ts": 1781234999.0, "is_open": true, "close_reason": null, "equivalent_bets": false, "customer_username": "user1", "customer_ccy": "USDT", "betslip_type": "normal", "price_list": [{"effective": {"price": 7.4, "min": ["USDT", 5.769], "max": ["USDT", 92.304]}}, {"effective": {"price": 4.2, "min": ["USDT", 3.4614], "max": ["USDT", 173.07]}}, {"effective": {"price": 2.1, "min": null, "max": ["USDT", 346.14]}}], "total": ["USDT", 611.514], "user_data": null}}}, "normal_single_level": {"summary": "Normal - single price level", "value": {"status": "ok", "data": {"betslip_id": "bs-single-001", "sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "for,h", "bet_type_description": "Home", "live_score": null, "expiry_ts": 1781234567.0, "is_open": true, "close_reason": null, "equivalent_bets": true, "customer_username": "user1", "customer_ccy": "USDT", "betslip_type": "normal", "price_list": [{"effective": {"price": 3.25, "min": ["USDT", 3.4614], "max": ["USDT", 519.21]}}], "total": ["USDT", 519.21], "user_data": null}}}, "lay": {"summary": "Lay - betting against", "value": {"status": "ok", "data": {"betslip_id": "e54ac4c325c64ba2a5eefc08b3408b49", "sport": "fb", "event_id": "2026-03-17,22,328", "bet_type": "against,h", "bet_type_description": "Home", "live_score": null, "expiry_ts": 1789700000.0, "is_open": true, "close_reason": null, "equivalent_bets": false, "customer_username": "user2", "customer_ccy": "USDT", "betslip_type": "lay", "price_list": [{"effective": {"price": 2.26, "min": ["USDT", 0.1128], "max": ["USDT", 10685.0521]}}, {"effective": {"price": 2.24, "min": ["USDT", 0.1128], "max": ["USDT", 5191.5088]}}, {"effective": {"price": 2.22, "min": ["USDT", 0.1128], "max": ["USDT", 1833.0947]}}], "total": ["USDT", 17709.6556], "user_data": null}}}, "parlay": {"summary": "Parlay - accumulator", "value": {"status": "ok", "data": {"betslip_id": "0e644fdcce7f45c0997427410703efb6", "sport": "parlay", "event_id": "", "bet_type": "parlay", "bet_type_description": "Karlsruher SC AND Draw AND SV Darmstadt 1898 AND Anastasia Potapova", "live_score": null, "expiry_ts": 1775837930.718644, "is_open": true, "close_reason": null, "equivalent_bets": false, "customer_username": "user3", "customer_ccy": "USDT", "betslip_type": "parlay", "price_list": [], "total": null, "legs": [{"id": 1, "sport": "fb", "event_id": "2026-04-10,224,204", "bet_type": "for,h", "live_score": null, "bet_type_description": "Karlsruher SC", "price": null, "outcome": null}, {"id": 2, "sport": "fb", "event_id": "2026-04-11,1027,211", "bet_type": "for,d", "live_score": null, "bet_type_description": "Draw", "price": null, "outcome": null}, {"id": 3, "sport": "fb", "event_id": "2026-04-11,1080,205", "bet_type": "for,h", "live_score": null, "bet_type_description": "SV Darmstadt 1898", "price": null, "outcome": null}, {"id": 4, "sport": "tennis", "event_id": "2026-04-10,80623,10056707", "bet_type": "for,tset,all,vset1,p1", "live_score": null, "bet_type_description": "Anastasia Potapova", "price": null, "outcome": null}]}}}, "no_prices": {"summary": "Quotes not yet arrived (poll again)", "value": {"status": "ok", "data": {"betslip_id": "bs-empty-005", "sport": "fb", "event_id": "2026-08-10,9009,1010", "bet_type": "for,a", "bet_type_description": "Away", "live_score": null, "expiry_ts": 1783000000.0, "is_open": true, "close_reason": null, "equivalent_bets": false, "customer_username": "user4", "customer_ccy": "USDT", "betslip_type": "normal", "price_list": [], "total": null, "user_data": null}}}}}}}}}, "delete": {"tags": ["Betslips"], "summary": "Close betslip", "description": "Close an open betslip. Its quotes stop, and it no longer counts toward the open-betslip cap.", "parameters": [{"name": "betslip_id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Betslip ID"}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "Betslip not found, expired, or already closed", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Betslip closed", "content": {"application/json": {"example": {"status": "ok", "data": null}}}}}}}, "/v2/betslips/{betslip_id}/refresh/": {"post": {"tags": ["Betslips"], "summary": "Refresh betslip", "description": "Extend the betslip expiration timeout.", "parameters": [{"name": "betslip_id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Betslip ID"}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Betslip expiration extended", "content": {"application/json": {"example": {"status": "ok", "data": null}}}}}}}, "/v2/orders/": {"get": {"tags": ["Orders"], "summary": "List orders", "description": "Returns a paginated list of orders for the authenticated customer.", "parameters": [{"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1, "default": 1}, "description": "Page number"}, {"name": "page_size", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 25}, "description": "Results per page"}, {"name": "status", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "description": "Filter by status (open, pending, done, failed)"}, {"name": "sport", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "description": "Filter by sport code - see \"Sports & bet types\" in the introduction."}, {"name": "event_id", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "description": "Filter by event ID"}, {"name": "order_type", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "description": "Filter by order type (normal, lay, parlay)"}, {"name": "date_from", "in": "query", "schema": {"type": "string", "format": "date-time"}, "description": "Start of date range (ISO 8601)"}, {"name": "date_to", "in": "query", "schema": {"type": "string", "format": "date-time"}, "description": "End of date range (ISO 8601)"}, {"name": "search", "in": "query", "schema": {"type": "string"}, "description": "Free-text search"}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Paginated order list", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderListEnvelope"}}}}}}, "post": {"tags": ["Orders"], "summary": "Create order", "description": "Places a new order on an existing betslip.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderCreateRequest"}, "examples": {"normal": {"summary": "Normal order", "value": {"betslip_id": "bs-single-001", "price": 3.25, "stake": ["USDT", 100.0], "duration": 5.0, "exchange_mode": "make_and_take"}}, "with_options": {"summary": "With optional fields", "value": {"betslip_id": "bs-single-001", "price": 3.25, "stake": ["USDT", 100.0], "duration": 10.0, "exchange_mode": "take_only", "keep_open_ir": true, "user_data": "my-ref-001", "accept_better_price": true, "accept_partial_fill": true}}, "exclude_danger": {"summary": "Order with dangerball liquidity excluded", "value": {"betslip_id": "bs-single-001", "price": 3.25, "stake": ["USDT", 100.0], "duration": 5.0, "exclude_danger": true}}}}}}, "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "400": {"description": "Validation failed", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"non_field_errors": ["event_stake_limit_reached"]}}}}}}, "409": {"description": "Idempotency conflict - `request_uuid` already used", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "order_already_created", "data": {"order_id": 5001, "message": "An order with this request_uuid already exists."}}}}}, "201": {"description": "Order created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderEnvelope"}, "example": {"status": "ok", "data": {"order_id": 5001, "order_type": "normal", "bet_type": "for,h", "bet_type_description": "Home", "sport": "fb", "want_price": 3.25, "want_stake": ["USDT", 115.38], "ccy_rate": 1.0, "placement_time": "2026-06-15T12:00:00+00:00", "expiry_time": "2026-06-15T12:05:00+00:00", "closed": false, "close_reason": null, "status": "open", "keep_open_ir": false, "exchange_mode": "make_and_take", "price": null, "stake": null, "profit_loss": null, "bet_bar_values": null, "current_score": null, "legs": null, "event_info": {"event_type": "normal", "event_id": "2026-06-15,1001,2002", "event_name": "Arsenal vs. Chelsea", "home_id": 1001, "home_team": "Arsenal", "away_id": 2002, "away_team": "Chelsea", "competition_id": 1, "competition_name": "England Premier League", "competition_country": "XE", "start_time": "2026-06-15T15:00:00+00:00", "date": "2026-06-15", "result": null}, "bets": [], "user_data": null}}}}}}}}, "/v2/orders/updates/": {"get": {"tags": ["Orders"], "summary": "Order updates", "description": "Returns orders updated within the given time range. Both `updated_at_from` and `updated_at_to` must be **at least 60 seconds in the past**, and the window (`updated_at_to - updated_at_from`) must not exceed **70 minutes**. For longer syncs, page through successive 70-minute windows.", "parameters": [{"name": "updated_at_from", "in": "query", "required": true, "schema": {"type": "string", "format": "date-time"}, "description": "Start of update window (ISO 8601). Must be at least 60 seconds in the past."}, {"name": "updated_at_to", "in": "query", "required": true, "schema": {"type": "string", "format": "date-time"}, "description": "End of update window (ISO 8601). Must be at least 60 seconds in the past and within 70 minutes of `updated_at_from`."}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "400": {"description": "Time window violates the 60-second / 70-minute rules", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "examples": {"too_recent": {"summary": "Timestamp is not at least 60 seconds in the past", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"updated_at_from": ["updated_at_from too recent"], "updated_at_to": ["updated_at_to too recent"]}}}}, "window_too_wide": {"summary": "Window exceeds 70 minutes", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"non_field_errors": ["The date range cannot exceed 70 minutes"]}}}}}}}}, "200": {"description": "Recently updated orders", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderListEnvelope"}}}}}}}, "/v2/orders/{order_id}/": {"get": {"tags": ["Orders"], "summary": "Get order", "description": "Returns a single order by ID with all stakes in USDT.", "parameters": [{"name": "order_id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Order ID"}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "Order not found or not visible to this key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Single order", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderEnvelope"}, "example": {"status": "ok", "data": {"order_id": 5001, "order_type": "normal", "bet_type": "for,h", "bet_type_description": "Home", "sport": "fb", "want_price": 3.2, "want_stake": ["USDT", 115.38], "ccy_rate": 1.0, "placement_time": "2026-06-15T12:00:00+00:00", "expiry_time": "2026-06-15T12:05:00+00:00", "closed": true, "close_reason": "order_filled", "status": "done", "keep_open_ir": false, "exchange_mode": "make_and_take", "price": 3.2, "stake": ["USDT", 115.38], "profit_loss": ["USDT", 253.84], "bet_bar_values": null, "current_score": null, "legs": null, "event_info": {"event_type": "normal", "event_id": "2026-06-15,1001,2002", "event_name": "Arsenal vs. Chelsea", "home_id": 1001, "home_team": "Arsenal", "away_id": 2002, "away_team": "Chelsea", "competition_id": 1, "competition_name": "England Premier League", "competition_country": "XE", "start_time": "2026-06-15T15:00:00+00:00", "date": "2026-06-15", "result": {"ht_home": 0, "ht_away": 0, "ft_home": 2, "ft_away": 1}}, "bets": [{"bet_id": 1, "order_id": 5001, "order_ccy_rate": 1.0, "status": {"code": "success"}, "sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "for,h", "ccy_rate": 1.0, "want_price": 3.2, "got_price": 3.2, "want_stake": ["USDT", 115.38], "got_stake": ["USDT", 115.38], "profit_loss": ["USDT", 253.84], "reconciled": true, "exchange_role": "taker"}], "user_data": null}}}}}}}}, "/v2/orders/tracked/{uuid}/": {"get": {"tags": ["Orders"], "summary": "Get order by UUID", "description": "Retrieve an order using the `request_uuid` from order creation instead of the order ID. Available for several days after placement (until the order is purged upstream).", "parameters": [{"name": "uuid", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}, "description": "The request_uuid used when creating the order"}], "responses": {"400": {"description": "The path value is not a valid UUID", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"request_uuid": ["must be a uuid"]}}}}}}, "401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "No order with this `request_uuid`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Order found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderEnvelope"}}}}}}}, "/v2/orders/{order_id}/close/": {"post": {"tags": ["Orders"], "summary": "Close an order", "description": "Close (cancel) a single open order. The order's lifecycle update is delivered on the WebSocket as an `[\"order\", ...]` entry with `closed: true` and `close_reason: \"cancelled\"`. The response `data` field is always `null`.", "parameters": [{"name": "order_id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "400": {"description": "The order exists but is already closed or settled (`order_closed`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "order_closed", "data": null}}}}, "404": {"description": "The order id is unknown (`not_found`)."}, "200": {"description": "Order closed.", "content": {"application/json": {"example": {"status": "ok", "data": null}}}}}}}, "/v2/orders/close_many/": {"post": {"tags": ["Orders"], "summary": "Close multiple orders", "description": "Close multiple orders synchronously. Maximum 500 order IDs per request.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["order_ids"], "properties": {"order_ids": {"type": "array", "minItems": 1, "maxItems": 500, "items": {"type": "integer"}}}}, "example": {"order_ids": [5001, 5002, 5003]}}}}, "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Close results. `closed` and `not_found` are each a list of order-id lists (grouped by the upstream batch); already-closed and unknown ids both land in `not_found`.", "content": {"application/json": {"schema": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"type": "object", "properties": {"closed": {"type": "array", "items": {"type": "array", "items": {"type": "integer"}}}, "not_found": {"type": "array", "items": {"type": "array", "items": {"type": "integer"}}}}}}}, "example": {"status": "ok", "data": {"closed": [[5001, 5002]], "not_found": [[5003]]}}}}}}}}, "/v2/orders/close_all/": {"post": {"tags": ["Orders"], "summary": "Close all open orders", "description": "Request cancellation of all open orders. Optionally filter by sport and/or event.", "requestBody": {"required": false, "content": {"application/json": {"schema": {"type": "object", "properties": {"sport": {"type": "string", "maxLength": 12, "description": "Only close orders on this sport - see \"Sports & bet types\" in the introduction."}, "event_id": {"type": "string", "description": "Only close orders on this event (requires sport)"}}}, "examples": {"all": {"summary": "Close all", "value": {}}, "by_sport": {"summary": "Close all football", "value": {"sport": "fb"}}, "by_event": {"summary": "Close specific event", "value": {"sport": "fb", "event_id": "2026-06-15,1001,2002"}}}}}}, "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "202": {"description": "Cancellation accepted. `data` is a list of the order-id lists being cancelled; an empty result is `[[]]`.", "content": {"application/json": {"example": {"status": "ok", "data": [[5001, 5002]]}}}}}}}, "/v2/orders/position/": {"get": {"tags": ["Orders"], "summary": "Calculate position", "description": "Calculate profit/loss position based on filtered orders. Accepts the same query parameters as the list orders endpoint.", "parameters": [{"name": "status", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}}, {"name": "sport", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}}, {"name": "event_id", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}}, {"name": "order_type", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}}, {"name": "date_from", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "date_to", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "search", "in": "query", "schema": {"type": "string"}}, {"name": "include_cashout_info", "in": "query", "description": "Accepted for compatibility. `cashout_info` is currently returned on every position regardless of this flag.", "schema": {"type": "boolean", "default": false}}], "responses": {"400": {"description": "The filters match orders from more than one event or sport", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"non_field_errors": ["filters match orders from multiple events"]}}}}}}, "401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Position calculation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PositionEnvelope"}, "example": {"status": "ok", "data": {"sport": "fb", "event_id": "2026-05-18,53672,10058814", "payoff_grid": {"ccy_code": "USDT", "values": [[-115.38, -115.38, -115.38], [173.07, -115.38, -115.38], [173.07, 173.07, -115.38]]}, "totals": {"for,h": {"bet_type_description": "Home", "got_price": 2.5, "got_stake": ["USDT", 115.38], "unknown_price": null, "unknown_stake": ["USDT", 0.0]}}, "unknown_bets_num": 0, "unknown_grid": null}}}}}}}}, "/v2/balance/": {"get": {"tags": ["Account"], "summary": "Account balance", "description": "Returns the authenticated user's current balance, total stake on open bets, and smart credit. All values are `[\"USDT\", amount]` tuples. The same three figures are pushed on the stream as the `balance` message; `open_stake` is positive.", "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Account balance", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BalanceEnvelope"}, "examples": {"with_smart_credit": {"summary": "Account with smart credit", "value": {"status": "ok", "data": {"balance": ["USDT", 10000.1], "open_stake": ["USDT", 152.55], "smart_credit": ["USDT", 1000.0]}}}, "without_smart_credit": {"summary": "Account without smart credit", "value": {"status": "ok", "data": {"balance": ["USDT", 10000.1], "open_stake": ["USDT", 152.55], "smart_credit": null}}}}}}}}}}, "/v2/heartbeats/": {"post": {"tags": ["Heartbeats"], "summary": "Create heartbeat", "description": "Open a new heartbeat timer. If the timer expires before being refreshed or cancelled, every open order on the account is closed, including orders placed by other sessions or API keys; only orders created after the expiry instant are spared. Closing cancels the unfilled stake and withdraws unmatched liquidity advertised on the exchange; bets that already matched are unaffected. Expiry is evaluated server-side about once per second.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["timeout"], "properties": {"timeout": {"type": "integer", "minimum": 10, "maximum": 300, "description": "Seconds before the heartbeat expires"}}}, "example": {"timeout": 60}}}}, "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "400": {"description": "`timeout` outside the 10-300 second range", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"timeout": ["Ensure this value is less than or equal to 300."]}}}}}}, "200": {"description": "Heartbeat created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HeartbeatEnvelope"}, "example": {"status": "ok", "data": {"heartbeat_id": "hb-abc123", "expiry_time": "2026-06-15T12:01:00+00:00"}}}}}}}, "get": {"tags": ["Heartbeats"], "summary": "List heartbeats", "description": "Returns all currently open heartbeats. **Note:** unlike other list endpoints (`/v2/orders/`, `/v2/betslips/`) which return a flat array under `data`, this endpoint wraps the array under `data.heartbeats` for historical reasons. Clients must special-case this shape.", "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Open heartbeats", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HeartbeatListEnvelope"}, "example": {"status": "ok", "data": {"heartbeats": [{"heartbeat_id": "hb-abc123", "expiry_time": "2026-06-15T12:01:00+00:00"}]}}}}}}}}, "/v2/heartbeats/{heartbeat_id}/": {"get": {"tags": ["Heartbeats"], "summary": "Get heartbeat", "description": "Returns information about a single heartbeat.", "parameters": [{"name": "heartbeat_id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "Heartbeat not found or not visible to this key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Heartbeat info", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HeartbeatEnvelope"}, "example": {"status": "ok", "data": {"heartbeat_id": "hb-abc123", "expiry_time": "2026-06-15T12:01:00+00:00"}}}}}}}, "delete": {"tags": ["Heartbeats"], "summary": "Cancel heartbeat", "description": "Cancel an active heartbeat. Cancelling disarms the timer without closing any orders; only expiry triggers the order close-out. The response `data` field is always `null`.", "parameters": [{"name": "heartbeat_id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "Heartbeat not found or already cancelled", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Heartbeat cancelled", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HeartbeatCancelEnvelope"}, "example": {"status": "ok", "data": null}}}}}}}, "/v2/heartbeats/{heartbeat_id}/refresh/": {"post": {"tags": ["Heartbeats"], "summary": "Refresh heartbeat", "description": "Extend the heartbeat expiration timeout. A heartbeat that has already expired cannot be refreshed - its close-out has been triggered; open a new heartbeat instead.", "parameters": [{"name": "heartbeat_id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "404": {"description": "Heartbeat not found, expired, or cancelled", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "200": {"description": "Heartbeat extended", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HeartbeatEnvelope"}, "example": {"status": "ok", "data": {"heartbeat_id": "hb-abc123", "expiry_time": "2026-06-15T12:02:00+00:00"}}}}}}}}, "/v2/xrates/": {"get": {"tags": ["Reference"], "summary": "Exchange rates", "description": "Returns current exchange rates.", "responses": {"401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Exchange rate list", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/XRatesEnvelope"}, "example": {"status": "ok", "data": [{"ccy": "USD", "rate": 1.265}, {"ccy": "EUR", "rate": 1.1347}, {"ccy": "USDT", "rate": 1.1538}]}}}}}}}, "/v2/sports/{sport}/bet_types/{bet_type}/": {"get": {"tags": ["Reference"], "summary": "Bet type info", "description": "Returns information about a bet type including the win/loss payout grid.", "parameters": [{"name": "sport", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Sport code - see \"Sports & bet types\" in the introduction."}, {"name": "bet_type", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Bet type string - see \"Sports & bet types\" in the introduction."}, {"name": "home_team", "in": "query", "schema": {"type": "string"}, "description": "Home team name for display labels"}, {"name": "away_team", "in": "query", "schema": {"type": "string"}, "description": "Away team name for display labels"}], "responses": {"400": {"description": "The `bet_type` string did not parse. Note this endpoint uses the `invalid_bet_type` code with the offending value under `data.bet_type`, not the usual `validation_error` shape. It also cannot validate multirunner bet types (`for,win,...`, `for,top,...`), which need an event context and return this error.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "invalid_bet_type", "data": {"bet_type": "for,notabettype"}}}}}, "401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "200": {"description": "Bet type information", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BetTypeInfoEnvelope"}, "example": {"status": "ok", "data": {"sport": "Football", "bet_type_description": "Away", "winloss_grid": [["l", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w"], ["l", "l", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w"], ["l", "l", "l", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w"], "... 20x20 grid of w/l values"]}}}}}}}}, "/v2/events/": {"get": {"tags": ["Market data"], "summary": "List events", "description": "Lists the events that currently have prices. Each entry is the object the stream sends as `[\"event\", {...}]`.\n\nFor price-sensitive work, use the streaming API: it pushes each change as it happens.\n\n`ir_status` keeps the events in running (`in_running`) or not yet started (`pre_event`), the same values each event carries.\n\n`event_type` gives the shape. A `normal` (match) event has `home` and `away`. A `multirunner` (outright) event has `teams` and `end_time`.\n\nPage with `limit` and `after`: pass the previous page's last `\"{sport},{event_id}\"` as `after`. A page shorter than `limit` is the last one. The response has no total count.\n\n`event_id` and `competition_id` filters need `sport`; a request without it gets a 400. Every entry has its `sport`, so a follow-up call can always supply it. Paging without filters needs no `sport`.\n\nA filter that matches no event returns an empty `data`. This includes a sport, competition or event that has no prices at the moment. Each `event_id` must have the form an event carries, such as `2026-06-15,1001,2002`; any other form gets a 400.", "parameters": [{"name": "sport", "in": "query", "description": "Sport codes to keep. Repeatable, up to 100 values; omit for every sport. Required alongside `event_id` or `competition_id`. See \"Sports & bet types\" in the introduction.", "schema": {"type": "array", "items": {"type": "string"}, "maxItems": 100}, "example": ["fb"]}, {"name": "competition_id", "in": "query", "description": "Competitions to keep. Repeatable, up to 100 values; omit for every competition. Requires `sport`.", "schema": {"type": "array", "items": {"type": "integer", "minimum": 0, "maximum": 2147483647}, "maxItems": 100}}, {"name": "event_id", "in": "query", "description": "Events to keep. Repeatable, up to 100 values; omit for every event. Requires `sport`. Each value is an event's `event_id`, such as `2026-06-15,1001,2002`; any other form gets a 400.", "schema": {"type": "array", "items": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2},[^,]+,[^,]+$"}, "maxItems": 100}}, {"name": "lang", "in": "query", "description": "Language for competition and team names.", "schema": {"type": "string", "enum": ["en", "zh-hans", "ko"], "default": "en"}}, {"name": "limit", "in": "query", "description": "Events per page.", "schema": {"type": "integer", "minimum": 1, "maximum": 5000, "default": 500}}, {"name": "after", "in": "query", "description": "Cursor - the previous page's last `\"{sport},{event_id}\"`.", "schema": {"type": "string"}, "example": "fb,2026-06-15,1001,2002"}, {"name": "start_time_from", "in": "query", "description": "Keep events starting at or after this instant, 1970 or later. An event whose start time is unknown is never inside a window.", "schema": {"type": "string", "format": "date-time"}, "example": "2026-06-15T00:00:00Z"}, {"name": "start_time_to", "in": "query", "description": "Keep events starting before this instant, 1970 or later. The window is half-open, so an event starting exactly at this instant is excluded.", "schema": {"type": "string", "format": "date-time"}, "example": "2026-06-16T00:00:00Z"}, {"name": "ir_status", "in": "query", "description": "Keep events with this `ir_status`. Omit for both.", "schema": {"type": "string", "enum": ["pre_event", "in_running"]}}], "responses": {"400": {"description": "Invalid query parameter.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"sport": ["sport is required when filtering by event_id or competition_id"]}}}}}}, "401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403Feed"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "503": {"$ref": "#/components/responses/Error503Feed"}, "200": {"description": "Events with prices", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EventListEnvelope"}, "examples": {"normal": {"summary": "Match event", "value": {"status": "ok", "data": [{"event_type": "normal", "sport": "fb", "event_id": "2026-06-15,1001,2002", "competition_id": 1, "competition_name": "England Premier League", "competition_country": "XE", "home": "Arsenal", "away": "Chelsea", "event_name": "Arsenal vs. Chelsea", "ir_status": "pre_event", "start_time": "2026-06-15T15:00:00Z"}]}}, "multirunner": {"summary": "Outright event", "value": {"status": "ok", "data": [{"event_type": "multirunner", "sport": "af", "event_id": "2026-02-23,multirunner,100364405", "competition_id": 545, "competition_name": "USA NFL", "competition_country": "US", "teams": [{"team_id": 21614, "name": "Arizona Cardinals"}, {"team_id": 21615, "name": "Atlanta Falcons"}], "event_name": "NFL Super Bowl Winner", "ir_status": "pre_event", "start_time": "2026-02-23T21:00:00Z", "end_time": "2027-02-14T21:00:00Z"}]}}}}}}}}}, "/v2/offers/": {"get": {"tags": ["Market data"], "summary": "List offers for an event", "description": "Returns the stake available at each price, for each market on the named events. Each entry is the object the stream sends as `[\"offer\", {...}]`.\n\nA reply can be about 4 seconds behind the live prices: the stream updates every 2 seconds, and a client may cache the response for 2 seconds (`Cache-Control: private, max-age=2`). For live prices, register the event on the stream with `register_event`.\n\n`sport` and `event_id` are required, with up to 20 events per request. `GET /v2/events/` lists them.\n\n`bet_type` limits the response to the named markets. It takes the same strings as `POST /v2/betslips/`, so a `bet_type` from here goes into a betslip unchanged.\n\n`market_type` keeps the named market families, such as `ah`, `ou` or `wdw`. `min_liquidity` keeps a market when its `max` stakes, added up across its `price_list`, come to at least that many USDT.\n\nOffers come back in `event_id` order, then `bet_type` order. Page with `limit` and `after`: pass the previous page's last `\"{event_id},{bet_type}\"` as `after`. A page shorter than `limit` is the last one. Each page is read when it is requested, so for a consistent view of an event, use the stream.\n\nMarkets with no available stake are left out. `for` and `against` on one selection are two markets, each with its own `bet_type`. For a sport your account is not enabled for, the list is empty, as on the stream.\n\nAn event that has no prices at the moment, or that the feed does not know, returns no offers. The other events in the request still come back. Each `event_id` must have the form an event carries, such as `2026-06-15,1001,2002`; any other form gets a 400.", "parameters": [{"name": "sport", "in": "query", "required": true, "description": "Sport code. See \"Sports & bet types\" in the introduction.", "schema": {"type": "string"}, "example": "fb"}, {"name": "event_id", "in": "query", "required": true, "description": "Events to price. Repeatable, up to 20 per request. Each value is an event's `event_id`, such as `2026-06-15,1001,2002`; any other form gets a 400.", "schema": {"type": "array", "items": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2},[^,]+,[^,]+$"}, "minItems": 1, "maxItems": 20}, "example": ["2026-06-15,1001,2002"]}, {"name": "bet_type", "in": "query", "description": "Markets to keep. Repeatable, up to 100 values; omit for every market on the event. See \"Sports & bet types\" in the introduction.", "schema": {"type": "array", "items": {"type": "string"}, "maxItems": 100}}, {"name": "market_type", "in": "query", "description": "Market families to keep, as in an offer's `market_type`. Repeatable, up to 100 values; omit for every family.", "schema": {"type": "array", "items": {"type": "string"}, "maxItems": 100}, "example": ["ah", "ou"]}, {"name": "min_liquidity", "in": "query", "description": "USDT. Keep markets whose `max` stakes, added up across the `price_list`, come to at least this.", "schema": {"type": "number", "minimum": 0}, "example": 250}, {"name": "limit", "in": "query", "description": "Offers per page.", "schema": {"type": "integer", "minimum": 1, "maximum": 5000, "default": 500}}, {"name": "after", "in": "query", "description": "Cursor - the previous page's last `\"{event_id},{bet_type}\"`.", "schema": {"type": "string"}, "example": "2026-06-15,1001,2002,for,ah,h,1"}], "responses": {"400": {"description": "Missing or invalid query parameter.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "examples": {"missing_event": {"summary": "event_id omitted", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"event_id": ["Field required"]}}}}, "too_many_events": {"summary": "More than 20 events requested", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"event_id": ["List should have at most 20 items after validation, not 21"]}}}}}}}}, "401": {"$ref": "#/components/responses/Error401"}, "403": {"$ref": "#/components/responses/Error403Feed"}, "429": {"$ref": "#/components/responses/Error429"}, "500": {"$ref": "#/components/responses/Error500"}, "503": {"$ref": "#/components/responses/Error503Feed"}, "200": {"description": "Offers for the named events", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OfferListEnvelope"}, "example": {"status": "ok", "data": [{"sport": "fb", "event_id": "2026-06-15,1001,2002", "bet_type": "for,ah,h,1", "market_type": "ah", "in_running": false, "price_list": [{"effective": {"price": 2.0, "min": null, "max": ["USDT", 150.0]}}, {"effective": {"price": 1.99, "min": null, "max": ["USDT", 80.0]}}]}]}}}}}}}, "/v2/stream": {"get": {"tags": ["Streaming API"], "summary": "WebSocket stream", "description": "**WebSocket endpoint** - upgrade an HTTP connection to receive\nreal-time prices. This is a separate service from the REST API.\n\n### Connection\n\nAuthenticate with an API key:\n\n```\nws://<host>/v2/stream?api_key=<api_key>\n```\n\n**Query parameters:**\n\n| Param | Required | Description |\n|-------|----------|-------------|\n| `api_key` | yes | API key (the `X-Api-Key` value) |\n| `lang` | no | Language code: `en` (default), `ko`, `zh-hans` |\n\nThe server does not restrict the `Origin` header, so browser-based\nclients - including pages opened from `file://` - can connect\ndirectly.\n\n### Wire format\n\n**Every** frame sent by the server is a batch envelope:\n\n```json\n{\"ts\": 1586042815.269000, \"data\": [ <message>, <message>, ... ]}\n```\n\n- `ts` - Unix timestamp in seconds with microsecond precision,\n  stamped by the server when the frame is written.\n- `data` - one or more messages. The `[\"offer\", ...]`,\n  `[\"response\", ...]`, `[\"event\", ...]`,\n  `[\"remove_event\", ...]`, `[\"sync\", ...]`, live-event-state and\n  account-update arrays shown below are the individual `data[]`\n  entries; they are never sent as bare top-level frames.\n\nMultiple messages may be batched into a single envelope - e.g. a\nregister snapshot and its ok `[\"response\", ...]` together, or offers alongside\naccount updates. Batching boundaries are not semantically\nmeaningful - iterate `data[]` and dispatch on each entry's leading\ntype tag (`entry[0]`); never rely on ordering, grouping, or a type\nappearing exactly once per frame.\n\n### Initial sync\n\nAfter connecting, the server sends a snapshot of the\ncurrently-priced events followed by a `[\"sync\", {...}]` marker\nwhose payload carries the `session_id` of this stream (useful\nwhen correlating with REST errors or contacting support). Each\nevent is an `[\"event\", {...}]` entry - a flat object carrying the\nidentifiers and metadata - delivered inside the envelope:\n\n```json\n{\"ts\": 1586042815.269000, \"data\": [\n  [\"event\", {\"event_type\": \"normal\", \"sport\": \"fb\", \"event_id\": \"2026-06-15,1001,2002\", \"competition_id\": 1, \"competition_name\": \"England Premier League\", \"competition_country\": \"XE\", \"home\": \"Arsenal\", \"away\": \"Chelsea\", \"event_name\": \"Arsenal vs. Chelsea\", \"ir_status\": \"pre_event\", \"start_time\": \"2026-06-15T15:00:00Z\"}],\n  [\"event\", {\"event_type\": \"normal\", \"sport\": \"tn\", \"event_id\": \"2026-06-16,501,502\", ...}],\n  [\"event\", {\"event_type\": \"multirunner\", \"sport\": \"af\", \"event_id\": \"2026-02-23,multirunner,100364405\", \"competition_id\": 545, \"competition_name\": \"USA NFL\", \"competition_country\": \"US\", \"teams\": [{\"team_id\": 21614, \"name\": \"Arizona Cardinals\"}, {\"team_id\": 21615, \"name\": \"Atlanta Falcons\"}], \"event_name\": \"NFL Super Bowl Winner\", \"start_time\": \"2026-02-23T21:00:00Z\", \"end_time\": \"2027-02-14T21:00:00Z\"}],\n  [\"sync\", {\"session_id\": \"...\"}]\n]}\n```\n\nTwo event shapes appear. A `normal` (match) event carries `home` and\n`away`. A `multirunner` (outright / futures) event has no `home`/`away`;\ninstead it carries a `teams` array (`[{\"team_id\", \"name\"}, ...]`, one per\nrunner) and an `end_time`. Dispatch on `event_type`; read the runner list\nfrom `teams` for multirunners.\n\nThe snapshot is not the full fixture list: it contains only events\nthat currently have live prices. The dump may span several\nenvelopes; `[\"sync\", ...]` is the last `data[]` entry of the final one.\nFrom then on, changed events are re-sent as `[\"event\", {...}]`\nentries, and an event whose prices disappear is delivered as\n`[\"remove_event\", {\"sport\": ..., \"event_id\": ..., ...}]`.\n\n### Live event state\n\nEvents that are in play may additionally produce state updates as\n`data[]` entries. Payloads are sport-specific; the football shapes:\n\n```json\n[\"event_time\", {\"sport\": \"fb\", \"event_id\": \"...\", \"time\": [\"1h\", 23]}]\n[\"event_score\", {\"sport\": \"fb\", \"event_id\": \"...\", \"score\": [1, 0]}]\n[\"event_red_cards\", {\"sport\": \"fb\", \"event_id\": \"...\", \"score\": [0, 1]}]\n```\n\n- `time` - `[period, minutes]`, where the football periods are\n  `\"1h\"`, `\"2h\"` and `\"ht\"`; `null` when no clock is available.\n- `score` - `[home, away]` (the `event_red_cards` payload reuses\n  the `score` key for the red-card counts).\n- `[\"ir_info\", {...}]` carries a full in-running state snapshot for\n  an event (fields vary by sport), and\n  `[\"remove_ir_info\", {\"sport\": ..., \"event_id\": ...}]` signals the\n  state is gone - treat both as informational.\n- `[\"event_exchange_dark_liquidity\", {\"sport\": ..., \"event_id\": ...,\n  \"lines\": {...}}]` - a rough estimate of additional liquidity\n  available per line on the event, beyond the published offers.\n  Informational.\n\n### Commands\n\n**Register for offers on an event:**\n```json\n[\"register_event\", \"<sport>\", \"<event_id>\"]\n```\n\nOn success the server immediately sends one `[\"offer\", ...]` per\nactive bet type on the event (the snapshot), then an ok\nresponse - typically batched in one envelope:\n`{\"ts\": ..., \"data\": [[\"offer\", {...}], [\"offer\", {...}],\n[\"response\", {\"status\": \"ok\", \"data\": null}]]}`.\nFrom then on, whenever the offers on the event change, the\nfull current set is re-broadcast (with the affected bet types\nupdated) and any bet type that has lost all liquidity is\ndelivered as `[\"remove_offer\", ...]`.\n\n**Unregister:**\n```json\n[\"unregister_event\", \"<sport>\", \"<event_id>\"]\n```\nServer responds with `[\"response\", {\"status\": \"ok\", \"data\":\nnull}]` - also when the event was not registered (unregistering is\nidempotent). No further `offer` / `remove_offer` messages are sent\nfor that event.\n\n**List currently registered events:**\n```json\n[\"list_registered_events\"]\n```\nServer responds with the full set of `(sport, event_id)` pairs\nthe session is registered for:\n```json\n[\"response\", {\"status\": \"ok\", \"data\": {\n  \"registered_events\": [\n    [\"fb\", \"2026-06-15,1001,2002\"],\n    [\"tennis\", \"2026-06-16,501,502\"]\n  ]\n}}]\n```\n\n**Keepalive (echo):**\n```json\n[\"echo\", \"any-payload\"]\n-> [\"response\", {\"status\": \"ok\", \"data\": [\"any-payload\"]}]\n```\nArguments are optional, may be any JSON values, and are echoed\nback verbatim in `data`. The server also sends an `[\"info\", ...]`\nentry every few seconds, so an idle connection still receives\nregular traffic.\n\n### `offer` / `remove_offer` messages\n\nEach `offer` describes the available stake at every price for one\n`(sport, event_id, bet_type)` triple. The `bet_type` string fully\nidentifies the market side (it encodes the market, handicap,\noutcome and for/against direction), so each triple is a distinct,\nindependently-updated offer - for and against on the same\nselection arrive as two separate `offer` messages with different\n`bet_type` values.\n\n```json\n[\"offer\", {\n  \"sport\": \"fb\",\n  \"event_id\": \"2026-06-15,1001,2002\",\n  \"bet_type\": \"for,ah,h,1\",\n  \"market_type\": \"ah\",\n  \"in_running\": false,\n  \"price_list\": [\n    {\"effective\": {\"price\": 2.0, \"min\": [\"USDT\", 5.0], \"max\": [\"USDT\", 150.0]}},\n    {\"effective\": {\"price\": 1.99, \"min\": null, \"max\": [\"USDT\", 80.0]}}\n  ]\n}]\n```\n\nEach `price_list` entry is\n`{\"effective\": {\"price\": <decimal>, \"min\": <stake|null>, \"max\": <stake>}}`;\nstakes are `[\"USDT\", amount]` arrays. The `min` and `max` keys are\nalways present:\n\n- `min` - the minimum stake accepted at that price; `null` when\n  there is no minimum.\n- `max` - the total stake available at that price. Always a\n  `[\"USDT\", amount]` pair (a price with no available stake is not\n  published).\n\nEntries are ordered by `price` (the decimal odds) **descending**,\nwith at most one entry per price.\n\n`remove_offer` carries only the `(sport, event_id, bet_type)`\ntriple - that bet type has no remaining liquidity for the event:\n\n```json\n[\"remove_offer\", {\n  \"sport\": \"fb\",\n  \"event_id\": \"2026-06-15,1001,2002\",\n  \"bet_type\": \"for,ah,h,1\"\n}]\n```\n\n### Account update messages\n\nAccount-level updates arrive on the same WebSocket, as plain\nentries inside the same `{\"ts\": ..., \"data\": [...]}` envelope that\ncarries `offer` / `remove_offer` - siblings of the market-data\nmessages.\n\n```json\n{\"ts\": 1586042815.269000, \"data\": [\n  [\"balance\", {\"balance\": [\"EUR\", 10000.1], \"open_stake\": [\"EUR\", 152.55], \"smart_credit\": [\"EUR\", 300.0]}],\n  [\"xrate\", {\"ccy\": \"EUR\", \"rate\": 1.1347}],\n  [\"order\", {...}],\n  [\"bet\", {...}],\n  [\"pmm\", {...}],\n  [\"betslip\", {...}],\n  [\"info\", {...}]\n]}\n```\n\n`data[]` may contain these account entry types:\n\n- `balance`, `xrate` - amounts are in your account's native\n  currency.\n- `order` - `want_stake`, `stake` and `profit_loss` are in USDT;\n  each entry of nested `bets[]` follows the `bet` format.\n- `bet` - `want_stake`, `got_stake`, `profit_loss` and\n  `status.response_pmm.effective.min`/`max` are in USDT.\n- `pmm`, `betslip` - the live quote and state of an open betslip\n  (see the Quickstart). `price_list` entries follow the same\n  `{\"effective\": {\"price\", \"min\", \"max\"}}` format as `offer`\n  messages; `price_list` and `total` are in USDT, prices sorted\n  descending.\n- `betslip_closed` - `{\"betslip_id\": ..., \"close_reason\": ...}`. The\n  betslip expired (betslips are short-lived) or was closed; no\n  further `pmm` quotes will arrive for it. Create a new betslip\n  to re-quote the selection.\n- `info` - feed status; `registered_events` is the number of\n  events currently registered on this connection.\n- `clear_events` - the server lost its upstream market data feed:\n  discard all event, offer and live-state data you hold. A fresh\n  snapshot (events, then `[\"sync\", ...]`) follows when the feed\n  recovers.\n\nThe order, presence, and count of entry types within `data[]` are\nnot contractual - the example above shows one possible ordering\nonly. Dispatch on each entry's type tag.\n\n**`balance` message fields:**\n- `balance`: `[currency, amount]` - current account balance in the customer's native currency.\n- `open_stake`: `[currency, amount]` - total stake across all unsettled bets, in the same currency.\n- `smart_credit`: `[currency, amount]` - extra funds available for betting on\n  top of the balance, equal to the amount the account is expected to win from\n  the bets already placed. Absent when the account has no smart credit.\n\n### Errors\n\nRecoverable command-level errors arrive in-band on the open\nconnection. The socket stays up; you can keep sending commands.\nLike every other message, the error element is a `data[]` entry\ninside the envelope (possibly batched with other messages):\n\n```json\n{\"ts\": 1586042815.269000, \"data\": [[\"response\", {\"status\": \"error\", \"code\": \"<code>\"}]]}\n```\n\nCodes emitted directly by the stream:\n\n| Code | When |\n|------|------|\n| `bad_json` | Frame is not valid JSON, not a JSON array, or an empty array. |\n| `invalid_input` | The command name is not a string or not recognised, or its argument shape is wrong for the command. |\n| `already_registered` | `register_event` for an event already registered on this session. |\n| `customer_event_limit_exceeded` | `register_event` would exceed your registered-events limit (counted across all your connections). Unregister something first. |\n| `invalid_customer` | `register_event` while the feed does not recognise your customer record (e.g. not yet propagated after a server restart). Retry after a short backoff; contact support if it persists. |\n| `system_error` | Transient server-side failure - retry after a short backoff. |\n\nNote there is no \"unknown event\" error: registering an event the\nfeed has no prices for succeeds with an empty snapshot, and\n`unregister_event` of an unregistered event returns ok. Treat any\nother code string as **opaque** - log it and retry after a short\nbackoff.\n\n**Authentication and admission are enforced at the HTTP handshake.**\nA refused upgrade returns a non-101 status with a short `text/plain`\nbody naming the reason. Clients see a handshake error.\n\n| Status | Body | What to do |\n|--------|------|------------|\n| 400 | `missing_credentials`, `invalid_lang` | `api_key` is missing, or `lang` is not an allowed value. Fix the request. |\n| 401 | `auth_rejected` | The key was not accepted. Do not retry with the same key; check it against a REST endpoint. |\n| 503 | `unavailable` | The server cannot accept connections right now. Retry with backoff. |\n\nTreat all other 4xx as 401 and all other 5xx as 503. The `websockets`\nlibrary raises `InvalidStatus` with the body on `exc.response.body`.\n\n**Backpressure** - if you read too slowly, the server's outbound\nbuffer overflows and the server closes with code **1008**. Read\nfaster or register fewer events. An immediate reconnect overflows\nagain. A client that stopped reading never receives the frame and\nsees only the raw TCP close.\n\nTwo classes of failure drop an *established* connection silently\n(raw TCP close, no WebSocket close frame, no in-band error):\n\n- **I/O error** - any read or write failure on the socket.\n- **Internal error** - a rare server-side failure; not\n  client-triggerable and observably identical to an I/O error.\n  Reconnect with backoff.\n", "parameters": [{"name": "api_key", "in": "query", "required": true, "schema": {"type": "string"}, "description": "API key (the `X-Api-Key` value)"}, {"name": "lang", "in": "query", "schema": {"type": "string", "enum": ["en", "ko", "zh-hans"], "default": "en"}, "description": "Language for event/competition names"}], "responses": {"101": {"description": "Switching protocols - WebSocket connection established"}, "400": {"description": "Refused at the handshake - `api_key` missing or `lang` invalid."}, "401": {"description": "Refused at the handshake - the key was not accepted."}, "503": {"description": "Refused at the handshake - the server cannot accept connections right now; retry with backoff."}, "default": {"description": "Failures are delivered in-band as `[\"response\", {\"status\": \"error\", \"code\": ...}]` entries inside the `{\"ts\": ..., \"data\": [...]}` envelope on the open WebSocket, or by silent TCP close - see the **Errors** section above."}}}}}, "components": {"securitySchemes": {"ApiKeyHeader": {"type": "apiKey", "in": "header", "name": "X-Api-Key", "description": "API key created from the magic-markets website"}}, "responses": {"Error400": {"description": "Validation failed - see `data.validation_errors`.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "examples": {"field_error": {"summary": "Field-level validation", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"bet_type": ["invalid_bet_type"]}}}}, "non_field_error": {"summary": "Cross-field validation failure", "value": {"status": "error", "code": "validation_error", "data": {"validation_errors": {"non_field_errors": ["event_not_found"]}}}}}}}}, "Error401": {"description": "Authentication failed - API key missing, malformed, or rejected.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "auth_error", "data": {"detail": "Authentication credentials were not provided."}}}}}, "Error403": {"description": "Authorization failed - the key is valid but cannot perform this action.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "forbidden", "data": null}}}}, "Error403Feed": {"description": "`invalid_customer`: the feed has no record of your account; contact support, because a retry gets the same answer. `forbidden`: the key is valid but cannot perform this action.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "invalid_customer", "data": null}}}}, "Error404": {"description": "Resource not found or not visible to this key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "not_found", "data": null}}}}, "Error429": {"description": "Rate limit hit. Honour the `Retry-After` header.", "headers": {"Retry-After": {"description": "Seconds to wait before retrying.", "schema": {"type": "integer"}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "throttled", "data": {"message": "Request was throttled. Expected available in 12 seconds.", "retry_after": 12}}}}}, "Error503Feed": {"description": "`warming`: the feed is loading its market data or customer records; retry with backoff. A body of `{ \"detail\": \"Service unavailable\" }` without the envelope means an upstream service could not be reached; retry with backoff.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "warming", "data": null}}}}, "Error500": {"description": "Unexpected internal error. Quote `data[1]` (the token) when contacting support.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorEnvelope"}, "example": {"status": "error", "code": "server_error", "data": ["An error has occurred, token:", "ABC123abc456DEF789gh"]}}}}}, "schemas": {"StakeTuple": {"type": "array", "description": "[currency, amount] - e.g. [\"USDT\", 115.38]", "prefixItems": [{"type": "string", "description": "Currency code (always USDT in responses)"}, {"type": "number", "description": "Amount"}], "items": {"type": ["string", "number"]}, "minItems": 2, "maxItems": 2}, "LiveScore": {"type": "object", "description": "In-running score at a point in time, `{home, away}`.", "required": ["home", "away"], "properties": {"home": {"type": "integer", "minimum": 0, "maximum": 1024, "description": "Home-team score"}, "away": {"type": "integer", "minimum": 0, "maximum": 1024, "description": "Away-team score"}}}, "ErrorEnvelope": {"type": "object", "description": "Standard error response. The shape of `data` varies by `code` - see the **Errors** section in the introduction and the named examples on each endpoint's error responses.", "required": ["status", "code"], "properties": {"status": {"type": "string", "enum": ["error"]}, "code": {"type": "string", "description": "Stable machine-readable error code (e.g. `validation_error`, `not_found`, `forbidden`).", "example": "validation_error"}, "data": {"description": "Variable. Null, string, or object - see examples."}}}, "Event": {"type": "object", "description": "An event that currently has prices, identical to the stream's `event` payload. `event_type` gives the shape: `normal` has `home` and `away`, `multirunner` has `teams` and `end_time`.", "properties": {"event_type": {"type": "string", "enum": ["normal", "multirunner"]}, "sport": {"type": "string", "description": "Sport code"}, "event_id": {"type": "string"}, "competition_id": {"type": "integer"}, "competition_name": {"type": "string"}, "competition_country": {"type": "string", "description": "Country code of the competition, for example `US`, or `XE` for England."}, "home": {"type": ["string", "null"], "description": "Normal events only"}, "away": {"type": ["string", "null"], "description": "Normal events only"}, "teams": {"description": "Runner list (multirunner events only)", "oneOf": [{"type": "array", "items": {"type": "object", "properties": {"team_id": {"type": "integer"}, "name": {"type": "string"}}}}, {"type": "null"}]}, "end_time": {"type": ["string", "null"], "format": "date-time", "description": "Race end time (multirunner events only)"}, "event_name": {"type": ["string", "null"]}, "ir_status": {"type": "string", "enum": ["pre_event", "in_running"]}, "start_time": {"type": ["string", "null"], "format": "date-time"}}}, "EventList": {"type": "array", "description": "Events that currently have prices.", "items": {"$ref": "#/components/schemas/Event"}}, "EventListEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/EventList"}}}, "Offer": {"type": "object", "description": "The stake available at each price for one market, identical to the stream's `offer` payload.", "properties": {"sport": {"type": "string"}, "event_id": {"type": "string"}, "bet_type": {"type": "string", "description": "The market and side: market type, line, outcome and `for` or `against`. `POST /v2/betslips/` accepts it as is."}, "market_type": {"type": "string", "description": "Market family the bet type belongs to (e.g. `ah`, `wdw`, `ou`)."}, "in_running": {"type": "boolean", "description": "Whether the event was in running when this offer was priced."}, "price_list": {"$ref": "#/components/schemas/PriceLevelList"}}}, "OfferList": {"type": "array", "description": "One entry per market.", "items": {"$ref": "#/components/schemas/Offer"}}, "OfferListEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/OfferList"}}}, "PriceLevel": {"type": "object", "properties": {"effective": {"type": "object", "properties": {"price": {"type": "number", "description": "Decimal price"}, "min": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Minimum stake at this price level, or null"}, "max": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Maximum stake at this price level, or null"}}}}}, "PriceLevelList": {"type": "array", "description": "Prices sorted descending (best price first).", "items": {"$ref": "#/components/schemas/PriceLevel"}}, "BetslipCreateResponse": {"type": "object", "description": "Create (POST) response; carries no prices. Poll GET or watch the stream for the quote.", "properties": {"betslip_id": {"type": "string"}, "sport": {"type": "string", "description": "Sport code - see \"Sports & bet types\" in the introduction."}, "event_id": {"type": "string", "description": "Event identifier, e.g. 2026-06-15,1001,2002"}, "bet_type": {"type": "string", "description": "Bet type string - see \"Sports & bet types\" in the introduction."}, "bet_type_description": {"type": "string", "description": "Human-readable label, e.g. Home, Over 1.5 (Asian)"}, "live_score": {"oneOf": [{"$ref": "#/components/schemas/LiveScore"}, {"type": "null"}], "description": "The in-running score the betslip was opened at, as supplied on create; null when none was given (pre-match selections)."}, "expiry_ts": {"type": "number", "description": "Unix timestamp when the betslip expires"}, "is_open": {"type": "boolean"}, "close_reason": {"type": ["string", "null"]}, "equivalent_bets": {"type": "boolean", "description": "Whether equivalent bets are included"}, "customer_username": {"type": "string"}, "customer_ccy": {"type": "string", "description": "Customer's base currency code"}, "betslip_type": {"type": "string", "enum": ["normal", "lay", "parlay"]}, "legs": {"description": "Parlay legs (only present for parlay betslips)", "oneOf": [{"$ref": "#/components/schemas/ParlayLegList"}, {"type": "null"}]}, "user_data": {"type": ["string", "null"], "maxLength": 512}}}, "BetslipResponse": {"allOf": [{"$ref": "#/components/schemas/BetslipCreateResponse"}, {"type": "object", "properties": {"price_list": {"type": "array", "description": "Prices sorted descending (best price first). May be empty until quotes arrive, or when no source is currently quoting.", "items": {"$ref": "#/components/schemas/PriceLevel"}}, "total": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Sum of max stakes across all price levels, or null if no prices"}}}]}, "BetslipCreateEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/BetslipCreateResponse"}}}, "BetslipEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/BetslipResponse"}}}, "BetslipListEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"type": "array", "items": {"type": "string"}, "description": "Array of betslip IDs"}}}, "BetslipCreateRequest": {"type": "object", "properties": {"sport": {"type": "string", "description": "Sport code (required for normal/lay) - see \"Sports & bet types\" in the introduction."}, "event_id": {"type": "string", "description": "Event ID (required for normal/lay)"}, "bet_type": {"type": "string", "description": "Bet type string (required for normal/lay) - see \"Sports & bet types\" in the introduction."}, "live_score": {"oneOf": [{"$ref": "#/components/schemas/LiveScore"}, {"type": "null"}], "description": "The in-running score you are selecting against, e.g. `{\"home\": 1, \"away\": 0}`. Liquidity a source is quoting at a different in-running score is then never treated as equivalent to your selection, so a line that has already moved on a goal cannot be matched against it. The score stays on the betslip and applies again when an order placed on it re-selects liquidity. Omit it (or send null) for pre-match selections and to accept quotes at whatever score a source is on."}, "legs": {"type": "array", "description": "Parlay legs (required for parlay, 2-10 legs)", "minItems": 2, "maxItems": 10, "items": {"type": "object", "required": ["sport", "event_id", "bet_type"], "properties": {"sport": {"type": "string"}, "event_id": {"type": "string"}, "bet_type": {"type": "string"}, "live_score": {"oneOf": [{"$ref": "#/components/schemas/LiveScore"}, {"type": "null"}], "description": "In-running score this leg is selected against, or null"}}}}, "betslip_type": {"type": "string", "enum": ["normal", "lay", "parlay"], "default": "normal"}, "equivalent_bets": {"type": "boolean", "default": true}, "user_data": {"type": ["string", "null"], "maxLength": 512}, "exclude_danger": {"type": "boolean", "default": false, "description": "When true, only liquidity sources that do not hold bets in danger status are used. When false or omitted, all available liquidity sources are used."}}}, "EventResultMatch": {"type": "object", "description": "Football and other two-half sports. `ht_*` are the half-time score and are omitted for single-period sports; `ft_*` are the full-time score.", "properties": {"ht_home": {"type": ["integer", "null"]}, "ht_away": {"type": ["integer", "null"]}, "ft_home": {"type": ["integer", "null"]}, "ft_away": {"type": ["integer", "null"]}}}, "EventResultTennis": {"type": "object", "description": "Tennis. `setN_pM` is player M's game count in set N.", "properties": {"set1_p1": {"type": ["integer", "null"]}, "set1_p2": {"type": ["integer", "null"]}, "set2_p1": {"type": ["integer", "null"]}, "set2_p2": {"type": ["integer", "null"]}, "set3_p1": {"type": ["integer", "null"]}, "set3_p2": {"type": ["integer", "null"]}, "set4_p1": {"type": ["integer", "null"]}, "set4_p2": {"type": ["integer", "null"]}, "set5_p1": {"type": ["integer", "null"]}, "set5_p2": {"type": ["integer", "null"]}, "who_retired": {"type": ["integer", "null"], "description": "1 or 2 if a player retired, else 0 or null"}}}, "EventResultHockey": {"type": "object", "description": "Ice hockey. `tpN_*` are the three period scores; `tall_*` the regulation total; `pen_*` the penalty-shootout score.", "properties": {"tp1_home": {"type": ["integer", "null"]}, "tp1_away": {"type": ["integer", "null"]}, "tp2_home": {"type": ["integer", "null"]}, "tp2_away": {"type": ["integer", "null"]}, "tp3_home": {"type": ["integer", "null"]}, "tp3_away": {"type": ["integer", "null"]}, "tall_home": {"type": ["integer", "null"]}, "tall_away": {"type": ["integer", "null"]}, "pen_home": {"type": ["integer", "null"]}, "pen_away": {"type": ["integer", "null"]}}}, "EventResultTableTennis": {"type": "object", "description": "Table tennis. `gameN_*` is the point score in game N (up to 7 games).", "properties": {"game1_home": {"type": ["integer", "null"]}, "game1_away": {"type": ["integer", "null"]}, "game2_home": {"type": ["integer", "null"]}, "game2_away": {"type": ["integer", "null"]}, "game3_home": {"type": ["integer", "null"]}, "game3_away": {"type": ["integer", "null"]}, "game4_home": {"type": ["integer", "null"]}, "game4_away": {"type": ["integer", "null"]}, "game5_home": {"type": ["integer", "null"]}, "game5_away": {"type": ["integer", "null"]}, "game6_home": {"type": ["integer", "null"]}, "game6_away": {"type": ["integer", "null"]}, "game7_home": {"type": ["integer", "null"]}, "game7_away": {"type": ["integer", "null"]}}}, "EventResultMultirunner": {"type": "object", "description": "Multirunner (outright) events.", "properties": {"runner_results": {"type": "array", "description": "Finishing positions per runner", "items": {"type": "object", "properties": {"team_id": {"type": "integer"}, "position": {"type": "integer", "description": "1=first, 2=second, 0=unknown, -1=void, -2=non-runner, -3=eliminated"}}}}, "non_runner_count": {"type": "integer"}}}, "EventInfo": {"type": "object", "properties": {"event_type": {"type": "string", "enum": ["normal", "multirunner", "parlay"]}, "event_id": {"type": ["string", "null"], "description": "null for parlay orders"}, "event_name": {"type": ["string", "null"]}, "home_id": {"type": ["integer", "null"], "description": "Normal events only"}, "home_team": {"type": ["string", "null"]}, "away_id": {"type": ["integer", "null"], "description": "Normal events only"}, "away_team": {"type": ["string", "null"]}, "competition_id": {"type": ["integer", "null"]}, "competition_name": {"type": ["string", "null"]}, "competition_country": {"type": ["string", "null"], "description": "ISO country code (e.g. XE for England)"}, "start_time": {"type": ["string", "null"], "format": "date-time"}, "date": {"type": ["string", "null"], "format": "date"}, "result": {"description": "Match or race result. The shape depends on the sport - see the `EventResult*` schemas. Null while the result is unknown.", "oneOf": [{"$ref": "#/components/schemas/EventResultMatch"}, {"$ref": "#/components/schemas/EventResultTennis"}, {"$ref": "#/components/schemas/EventResultHockey"}, {"$ref": "#/components/schemas/EventResultTableTennis"}, {"$ref": "#/components/schemas/EventResultMultirunner"}, {"type": "null"}]}, "teams": {"description": "Runner list (multirunner events only)", "oneOf": [{"type": "array", "items": {"type": "object", "properties": {"team_id": {"type": "integer"}, "name": {"type": "string"}}}}, {"type": "null"}]}, "end_time": {"type": ["string", "null"], "format": "date-time", "description": "Race end time (multirunner events only)"}, "leg_event_infos": {"type": ["array", "null"], "description": "Sub-event info for each leg (parlay orders only)", "items": {"$ref": "#/components/schemas/EventInfo"}}}}, "ParlayLegList": {"type": "array", "items": {"$ref": "#/components/schemas/ParlayLeg"}}, "ParlayLeg": {"type": "object", "properties": {"id": {"type": "integer"}, "sport": {"type": "string", "description": "Sport code - see \"Sports & bet types\" in the introduction."}, "event_id": {"type": "string"}, "bet_type": {"type": "string", "description": "Bet type string - see \"Sports & bet types\" in the introduction."}, "live_score": {"oneOf": [{"$ref": "#/components/schemas/LiveScore"}, {"type": "null"}], "description": "In-running score this leg was selected against, or null"}, "bet_type_description": {"type": "string"}, "price": {"type": ["number", "null"]}, "outcome": {"type": ["string", "null"], "enum": ["w", "l", "v", "v/w", "l/v", "l/w", "unknown", null], "description": "Leg settlement: `w` won, `l` lost, `v` void, `v/w` win-void, `l/v` void-loss, `l/w` loss-win (half outcomes), `unknown`, or null before settlement."}}}, "BetResponse": {"type": "object", "description": "Individual bet within an order", "properties": {"bet_id": {"type": "integer"}, "order_id": {"type": "integer"}, "order_ccy_rate": {"type": "number"}, "status": {"type": "object", "description": "Bet status", "properties": {"code": {"type": "string", "description": "e.g. matched, failed, settled"}, "reason": {"type": "string", "description": "Human-readable failure reason; present only when `code` is `failed`"}, "response_pmm": {"oneOf": [{"$ref": "#/components/schemas/PriceLevel"}, {"type": "null"}]}}}, "sport": {"type": "string", "description": "Sport code - see \"Sports & bet types\" in the introduction."}, "event_id": {"type": ["string", "null"]}, "bet_type": {"type": "string", "description": "Bet type string - see \"Sports & bet types\" in the introduction."}, "ccy_rate": {"type": "number"}, "want_price": {"type": "number"}, "got_price": {"type": ["number", "null"]}, "want_stake": {"$ref": "#/components/schemas/StakeTuple"}, "got_stake": {"$ref": "#/components/schemas/StakeTuple"}, "profit_loss": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}]}, "reconciled": {"type": ["boolean", "null"]}, "exchange_role": {"type": ["string", "null"], "enum": ["maker", "taker", null]}}}, "OrderResponse": {"type": "object", "properties": {"order_id": {"type": "integer"}, "order_type": {"type": "string", "description": "Order type. `normal`, `lay` and `parlay` are the types placed through this API; `brokerage`, `cashout` and `custom` can appear on orders created by other channels. Treat it as an open set.", "enum": ["normal", "lay", "parlay", "brokerage", "cashout", "custom"]}, "bet_type": {"type": "string", "description": "Bet type string - see \"Sports & bet types\" in the introduction."}, "bet_type_description": {"type": "string"}, "sport": {"type": "string", "description": "Sport code, or `parlay` for accumulators - see \"Sports & bet types\" in the introduction."}, "want_price": {"type": "number"}, "want_stake": {"$ref": "#/components/schemas/StakeTuple"}, "ccy_rate": {"type": "number", "description": "Exchange rate at order time"}, "placement_time": {"type": "string", "format": "date-time"}, "expiry_time": {"type": "string", "format": "date-time"}, "closed": {"type": "boolean"}, "close_reason": {"type": ["string", "null"], "description": "Reason the order closed, e.g. order_filled, timed_out; null while open"}, "event_info": {"oneOf": [{"$ref": "#/components/schemas/EventInfo"}, {"type": "null"}]}, "bets": {"type": "array", "items": {"$ref": "#/components/schemas/BetResponse"}}, "user_data": {"type": ["string", "null"]}, "status": {"type": "string", "description": "Order lifecycle status: open, pending, failed, partial_void, full_void, done, or reconciled"}, "keep_open_ir": {"type": "boolean"}, "exchange_mode": {"type": ["string", "null"], "enum": ["make_and_take", "take_only", "dark", null], "description": "Exchange interaction mode as passed at creation; null on older orders"}, "price": {"type": ["number", "null"], "description": "Achieved price (null while open)"}, "stake": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Aggregate stake across matched bets"}, "profit_loss": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}]}, "bet_bar_values": {"type": ["object", "null"]}, "current_score": {"oneOf": [{"type": "array", "prefixItems": [{"type": "integer", "description": "Home score"}, {"type": "integer", "description": "Away score"}], "items": {"type": "integer", "minimum": 0, "maximum": 32767}, "minItems": 2, "maxItems": 2}, {"type": "null"}], "description": "The in-running score the exchange held for the event when the order was placed, `[home, away]`; null for pre-match orders and for events without a running score."}, "legs": {"oneOf": [{"$ref": "#/components/schemas/ParlayLegList"}, {"type": "null"}]}}}, "OrderEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/OrderResponse"}}}, "OrderListEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/OrderResponse"}}}}, "OrderCreateRequest": {"type": "object", "required": ["betslip_id", "price", "stake"], "properties": {"betslip_id": {"type": "string"}, "price": {"type": "number", "description": "Limit decimal price. A bet is taken only at this price or better, and an off-tick limit moves to the first tick that honours it; see \"Price ticks\"."}, "stake": {"$ref": "#/components/schemas/StakeTuple"}, "duration": {"type": "number", "description": "Order duration in seconds (default 15)"}, "exchange_mode": {"type": "string", "enum": ["make_and_take", "take_only", "dark"], "default": "make_and_take", "description": "How the order interacts with the exchange. `make_and_take`: fill against the best available liquidity first; any remaining stake is advertised on the exchange at your price, while the order keeps taking newly available liquidity. `take_only`: only consume available liquidity; remaining stake is never advertised and other orders cannot match against it. `dark`: like `make_and_take`, but the advertised remaining stake is hidden from other customers - they can still match it when their price crosses yours, but they cannot see your price until then. There is no post-only mode: no order type rests in the book without also taking crossing liquidity."}, "keep_open_ir": {"type": "boolean", "default": false, "description": "Keep order open when event goes in-play"}, "user_data": {"type": ["string", "null"], "maxLength": 512}, "request_uuid": {"type": "string", "description": "Idempotency key"}, "accept_partial_fill": {"type": "boolean", "default": true}, "accept_better_price": {"type": "boolean", "default": true}, "force_want_price": {"type": "boolean", "default": false}, "min_taker_want_stake": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "`dark` orders only (rejected on other modes): the minimum total stake another order must request to be allowed to match yours at your price point. Cannot exceed the order's own stake. Set it non-zero to stop small probe orders from discovering your price."}, "current_score": {"oneOf": [{"type": "array", "prefixItems": [{"type": "integer", "description": "Home score"}, {"type": "integer", "description": "Away score"}], "items": {"type": "integer", "minimum": 0, "maximum": 32767}, "minItems": 2, "maxItems": 2}], "description": "Placement-time score assertion, `[home, away]`. When present, the order is rejected with `validation_error` / `non_field_errors: [\"event_scores_dont_match\"]` unless the value matches the live score the exchange holds for the event - use it to avoid placing on a price that has not reacted to a goal yet. Only meaningful for football: for sports without a goal-style running score, and while no score is known yet, the server assumes `[0, 0]` and rejects any other value. Omit the field to skip the check."}, "exclude_danger": {"type": "boolean", "default": false, "description": "When true, only liquidity sources that do not hold bets in danger status are used. When false or omitted, all available liquidity sources are used."}, "bookie_min_stakes": {"type": "object", "description": "Optional per-source minimum stakes, keyed by source. A source is only used if it can take at least its minimum. Values are `[currency, amount]` tuples.", "additionalProperties": {"$ref": "#/components/schemas/StakeTuple"}}, "placer_type": {"type": ["string", "null"], "description": "Optional caller-supplied tag recorded against the order."}}}, "BalanceResponse": {"type": "object", "required": ["balance", "open_stake", "smart_credit"], "properties": {"balance": {"$ref": "#/components/schemas/StakeTuple", "description": "Current account balance"}, "open_stake": {"$ref": "#/components/schemas/StakeTuple", "description": "Total stake on open bets, positive"}, "smart_credit": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Extra funds available for betting on top of the balance, equal to the amount the account is expected to win from the bets already placed. Null when the account has no smart credit."}}}, "BalanceEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/BalanceResponse"}}}, "HeartbeatResponse": {"type": "object", "properties": {"heartbeat_id": {"type": "string"}, "expiry_time": {"type": "string", "format": "date-time"}}}, "HeartbeatEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/HeartbeatResponse"}}}, "HeartbeatListEnvelope": {"type": "object", "description": "List response. Note the data is wrapped under a `heartbeats` key, unlike other list endpoints which return a flat array.", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"type": "object", "properties": {"heartbeats": {"type": "array", "items": {"$ref": "#/components/schemas/HeartbeatResponse"}}}}}}, "HeartbeatCancelEnvelope": {"type": "object", "description": "Cancel response. `data` is always `null`.", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"type": "null"}}}, "XRate": {"type": "object", "properties": {"ccy": {"type": "string", "description": "Currency code (e.g. USD, EUR, USDT)"}, "rate": {"type": "number", "description": "Exchange rate to USDT"}}}, "XRatesEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/XRate"}}}}, "BetTypeInfoResponse": {"type": "object", "properties": {"sport": {"type": "string", "description": "Display name for the sport (e.g. \"Football\")"}, "bet_type_description": {"type": "string", "description": "Human-readable bet type label"}, "winloss_grid": {"type": "array", "description": "20x20 grid of `w`/`l`/`p` (push)/`v` (void) outcomes", "items": {"type": "array", "items": {"type": "string"}}}}}, "BetTypeInfoEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/BetTypeInfoResponse"}}}, "PositionGrid": {"type": "object", "description": "Profit or loss per scoreline, in USDT: `values[home_score][away_score]`. The grid is square and its size depends on the sport.", "properties": {"ccy_code": {"type": "string", "description": "Always USDT"}, "values": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}}}, "required": ["ccy_code", "values"]}, "PositionComponentTotal": {"type": "object", "description": "Position for a standard bet type.", "properties": {"bet_type_description": {"type": "string", "description": "Bet type rendered for display"}, "got_price": {"type": ["number", "null"], "description": "Average matched price, or null if nothing matched"}, "got_stake": {"$ref": "#/components/schemas/StakeTuple"}, "unknown_price": {"type": ["number", "null"], "description": "Average price of the bets whose outcome is still unknown"}, "unknown_stake": {"$ref": "#/components/schemas/StakeTuple"}}, "required": ["bet_type_description", "got_price", "got_stake", "unknown_price", "unknown_stake"]}, "PositionCustomBetTotal": {"type": "object", "description": "Position for a custom bet type, which carries its own grid instead of prices.", "properties": {"bet_type_description": {"type": "string", "description": "Bet type rendered for display"}, "payoff_grid": {"oneOf": [{"$ref": "#/components/schemas/PositionGrid"}, {"type": "null"}]}, "got_stake": {"$ref": "#/components/schemas/StakeTuple"}}, "required": ["bet_type_description", "payoff_grid", "got_stake"]}, "PositionCashoutInfo": {"type": "object", "description": "Cashout valuation for the position. Present on every non-multirunner position. Cashout is offered on football only; on every other sport, and whenever cashout is otherwise unavailable, `allowed` is false and every value below is null.", "properties": {"allowed": {"type": "boolean"}, "reason": {"type": ["string", "null"], "description": "Why cashout is unavailable, when it can be shared: `position_already_flat` or `insufficient_credit`."}, "valuation": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Amount offered to close the position now"}, "stake": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Stake currently at risk on the position"}, "smart_credit_delta": {"oneOf": [{"$ref": "#/components/schemas/StakeTuple"}, {"type": "null"}], "description": "Change in smart credit that taking the cashout would cause"}, "position": {"oneOf": [{"$ref": "#/components/schemas/PositionGrid"}, {"type": "null"}], "description": "Payoff grid the cashout is valued against"}}, "required": ["allowed", "reason", "valuation", "stake", "smart_credit_delta", "position"]}, "PositionResponse": {"type": "object", "description": "Aggregate profit/loss position over the filtered orders. `sport`, `event_id` and `event_info` are present once the filters match at least one order.", "properties": {"payoff_grid": {"oneOf": [{"$ref": "#/components/schemas/PositionGrid"}, {"type": "null"}], "description": "Payoff from the matched bets, or null when every cell is zero"}, "totals": {"type": "object", "description": "Position per bet type, keyed by the unflipped bet type string", "additionalProperties": {"oneOf": [{"$ref": "#/components/schemas/PositionComponentTotal"}, {"$ref": "#/components/schemas/PositionCustomBetTotal"}]}}, "unknown_bets_num": {"type": "integer", "description": "Number of bets that could not be projected onto the grid"}, "unknown_grid": {"oneOf": [{"$ref": "#/components/schemas/PositionGrid"}, {"type": "null"}], "description": "Payoff from the bets counted in `unknown_bets_num`"}, "sport": {"type": "string"}, "event_id": {"type": "string"}, "event_info": {"$ref": "#/components/schemas/EventInfo"}, "cashout_info": {"$ref": "#/components/schemas/PositionCashoutInfo"}}, "required": ["payoff_grid", "totals", "unknown_bets_num", "unknown_grid"]}, "PositionEnvelope": {"type": "object", "properties": {"status": {"type": "string", "enum": ["ok"]}, "data": {"$ref": "#/components/schemas/PositionResponse"}}}}}}