{
  "openapi": "3.1.0",
  "info": {
    "title": "Parity API",
    "version": "1.0.0",
    "summary": "Embedded prediction markets, in two shapes: Parity Embed (a turnkey Predictions tab) and Parity Headless (this API, plus streaming, behind your own UI). The operator keeps the customer, the wallet and the brand in both.",
    "description": "## Two ways to integrate. Choose before you read further\n\n| | **Parity Embed** | **Parity Headless** |\n| --- | --- | --- |\n| What you ship | an `<iframe>` and one server endpoint | your own UI over this API |\n| Who draws the market list, ticket and positions | Parity | you |\n| Server work | mint an embed session | quote, order, reserve, settle, reconcile |\n| This document | the two `/embed-sessions` and order-lifecycle paths | all of it |\n| Recommended for | casinos and sportsbooks adding a Predictions tab | operators and fintechs with an existing trading UI |\n\n**Parity Embed is the recommended path** and the fastest to launch: `POST /api/v1/embed-sessions`\nfrom your backend, an iframe, and a `message` listener. See `/developers/embed`.\n\nBoth modes share one money architecture, and it is not negotiable in either.\n\n## The architecture, before anything else\n\n**The OPERATOR owns its customers’ cash ledger. Parity never holds player funds.**\n\nThere is no Parity player balance. There is no Parity deposit or withdrawal screen. There is\nno \"transfer to Predictions\" step, and there never will be: the customer’s money never\nleaves the one authoritative balance you already run.\n\nConcretely: a customer has **$100** with you. They stake $25 on a contract. Parity returns a\nquote whose `operatorMoney.authorizationAmount` is **`\"25.00\"`**. YOU reserve $25 in YOUR\nledger, so the customer now sees **$75 available, $25 reserved**. Parity is told only whether\nthat reserve succeeded or failed. From there Parity owns the order: routing, fills,\npositions, cashout, settlement, fees and reconciliation, and hands back exact amounts for\nyou to apply.\n\nIf you **cannot** reserve the `authorizationAmount`, decline the order. Parity shows an\ninsufficient-funds state, no fill is invented, and nothing is transmitted to a venue.\n\n| Parity owns | Parity never owns |\n| --- | --- |\n| catalogue, live pricing, quote, order, routing, fills, positions, cashout, settlement, fees, reconciliation, and the exact money instructions | player deposits, withdrawals, the general cash balance, the casino wallet, the customer account, KYC, support |\n\nEvery money-moving response carries EXACT INSTRUCTIONS to apply against the operator’s own\nledger: never a balance to read and never a figure to re-derive:\n\n| Response | Field | What the operator does |\n| --- | --- | --- |\n| quote / order (BUY) | `operatorMoney.authorizationAmount` | reserve this before the order |\n| order (BUY) | `operatorMoney.actualDebit` | take this once the fill is known |\n| order (BUY) | `operatorMoney.releaseAmount` | give this back: non-zero on a partial fill |\n| quote / order (SELL) | `operatorExitMoney.sellProceeds` | **credit** this. An exit never debits |\n| settlement | `operatorMoney.settlementCredit` | credit a winner |\n| settlement | `operatorMoney.voidCredit` | refund a cancelled market, fee included |\n\nTwo identities hold exactly, by construction rather than by tolerance:\n\n```\nauthorizationAmount = actualDebit + releaseAmount\nactualDebit         = tradeAmount + platformFee + venueFee\n```\n\nAn operator that debits the full stake on a partial fill is keeping money that is not theirs\nto keep. An operator that recomputes a debit from price × quantity has produced a second\nanswer to a question that already has one, and only one of them is in Parity’s ledger.\n\n## Money on the wire\n\nEvery amount inside `operatorMoney` / `operatorExitMoney` is an **exact decimal string**.\nA JSON number cannot carry an exact cent past a certain size and, worse, looks like it can.\nParse them as decimals or as integer minor units; never as a float.\n\nThe float fields beside them (`stake`, `notional`, `platformFee`, `averagePrice`,\n`executionPrice`) are for DISPLAY. Do not book from one.\n\n## Authentication and identity\n\n```\nAuthorization: Bearer <key>          the OPERATOR. `pk_test_…` sandbox, `sk_live_…` live\nX-Parity-User: <externalUserId>    the OPERATOR’S own id for the end user\n```\n\nScopes are enforced per route: `markets:read`, `quotes:write`, `orders:write`, `orders:read`,\n`positions:read`, `settlements:read`, `ledger:read`, `funding:write`. A key without the scope\ngets `403 insufficient_scope` **before** its rate limit is consumed, so a scope mistake never\narrives disguised as a 429 on the retry.\n\n## Environments\n\nSandbox is a property of the TENANT (`operators.mode`), not of a request: no header a caller\ninvents moves them across the boundary. Key spaces do not overlap. The sandbox runs the\nproduction pipeline with execution simulated: live prices, real quote expiry, real\nidempotency, real settlement, so a reconciliation written against it is the one that runs in\nproduction.\n\nThree groups of endpoints are **sandbox only** and answer `403 sandbox_only` in live mode:\n`/users/{id}/balance`, `/users/{id}/ledger` and `/funding/*`. They model a Parity-held\nper-user balance so that somebody learning the flow has simulated money to spend. That is not\nthe production integration path: the instruction contract above is.\n\n## Rate limits\n\nPer key, per minute (sandbox default 120). Every response carries `RateLimit-Limit`,\n`RateLimit-Remaining` and `RateLimit-Reset`, on success as well as refusal, so a client can\nsee its quota without having to hit the limit to discover it.\n\n## Idempotency\n\n`clientOrderId` is the operator’s own id for an order and is the idempotency key. A duplicate\nREPLAYS the original order (`200` with `idempotentReplay: true`) rather than opening a\nsecond position. It is enforced by a unique index on (operator, user, clientOrderId), not by a\ncheck in a handler. Derive it from the reservation in your own ledger, never from a clock: the\nwhole point is that it survives the crash that lost you our response.\n\n## Reference probability vs executable price\n\n`probability` / `yesCents` are the market’s REFERENCE: usually a midpoint that nobody trades\nat. `executableYesCents` / `executableNoCents` are what a trade would actually cost: a BUY\ntakes the ask and a SELL takes the bid. Over 22,586 open contracts the ask sits a median 1.5¢\nabove the reference and 24.8% differ by more than 10¢.\n\n**Render the reference as a probability and the executable as a price.** When the executable\nis `null` we hold no usable book side: show “no price” and never fall back to the\nreference.\n\nThe recurring **five-minute crypto contracts** always report `null` here, and `tradable:\nfalse` with it. They are fully tradeable: they are simply priced from the live order book at\nquote time, because a catalogue snapshot cannot keep up with a market that lives 300 seconds.\nFor those, `POST /quotes` is the only source of a price, and the price it returns is the one\nyou will be filled at.\n\n## Order lifecycle\n\n```\nPOST /quotes\n   │\n   ▼  POST /orders  { executionIntent: \"record_intent\" }\nreserve_pending             recorded, NOT YET EXECUTABLE\n   │\n   │  you reserve authorizationAmount in YOUR ledger\n   ▼  POST /orders/{id}/confirm-reserve\npending_venue_execution     reserve confirmed, cleared to execute\n   │\n   ▼\nsubmitted ─▶ partially_filled ─▶ filled | rejected   (a part fill stays `unknown` until the venue closes it)\n   │\n   └─▶ unknown after timeout ─▶ reconciliation_required\n```\n\nAn order is recorded before it can execute, and it becomes executable when you confirm that\nthe customer’s funds are held. The row is written first on purpose: an execution that\nsucceeds upstream and then fails to be recorded is the one outcome with no recovery path. An\norder reaching `pending_venue_execution` is a real, funded, recorded order. It is not a\ntrade, and `simulated` on the response says whether a fill was produced locally.\n\n`reserve_pending` has no transition to `submitted` in the execution state machine. That is\nthe guarantee expressed where it cannot be forgotten: not a check in a handler, a missing\nedge. No worker and no retry can transmit an order whose customer funds nobody confirmed\nwere held.\n\n`executionIntent: \"simulate\"` produces an immediate simulated fill and a simulated position.\nThat is **SANDBOX** behaviour. It is the default only for a tenant that is a declared\nsimulation, and it must never be conflated with the intent path above: the two produce\ndifferent facts (one creates a position and a debit, the other creates neither), which is\nwhy the field is an explicit request parameter rather than something you infer from the\nresponse.\n\nWhen the venue does not answer in time the order path returns `409 reconciliation_required`\nand leaves the row exactly as it is. **That is not a rejection.** The order is at the venue or\nit is not, and Parity does not yet know which; collateral stays committed and the\nauthorization stays held. `GET /orders/{id}` reports it honestly as `status: \"submitted\"` with\n`settlementState: \"pending\"`.\n\nReleasing the authorization there is the most expensive mistake available on this API: it\nleaves a real position with no cash behind it, and the operator cannot discover the error\nbecause we told them it was rejected. Poll\n`GET /orders?externalUserId=&clientOrderId=` (the id you chose) until `settlementState`\nreads `settled`.\n\n## Response envelopes\n\nEvery response on this surface is a JSON **object**. Nothing returns a bare array, and a\nsingle resource is wrapped rather than returned bare wherever the table below says `data`.\n\nThe envelopes are NOT uniform, and this table is the contract. They grew separately, partners\nconsume them as they are, and renaming a key now would break a working integration to buy\ntidiness, so the shapes stay and the ambiguity is removed here instead.\n\n| Endpoint | Items under | Pagination fields | End of collection |\n| --- | --- | --- | --- |\n| `GET /events` | `data` | `pagination.{limit,total,nextCursor,hasMore}` | `nextCursor: null`. A FULL page always mints a cursor, so the end may cost one final empty request |\n| `GET /events/{id}` | `data` (a single object) | none | none |\n| `GET /categories` | `data`, plus `totalEvents` | none: unpaged | the whole list, every call |\n| `GET /orders` | `orders`, plus `count` | `nextCursor`, `hasMore` | `nextCursor: null` **exactly**. The page is cut from a `limit + 1` read |\n| `GET /positions` | `positions`, plus `count` | `nextCursor`, `hasMore` | as `GET /orders` |\n| `GET /settlements` | `settlements` | `nextCursor`, `hasMore` | **`hasMore: false`**. The cursor stays non-null at the tail: it is a resumable watermark, not an end marker |\n| `GET /settlements/positions` | `settlements`, plus `count` | `cursor`: pass back as `?since=` | deprecated; use `GET /settlements` |\n| `GET /reconciliation/feed` | `items` | `nextCursor`, `hasMore` | **`hasMore: false`** means caught up. Poll again later with the SAME cursor |\n| `GET /reconciliation` | a single report object, not a collection | none | none |\n| `GET /orders/{id}`, `GET /positions/{id}` | the object itself, unwrapped | none | none |\n\nThree of those are worth stating twice, because each one has cost an integrator a day:\n\n- `GET /events/{id}` returns **`{ \"data\": { … } }`**, not the event object at the top level.\n- On `/settlements` and `/reconciliation/feed`, **stop on `hasMore`, never on the cursor.**\n  Looping `while (nextCursor)` there never terminates.\n- On `/orders` and `/positions`, `nextCursor: null` is exact and `hasMore` says the same thing.\n  `hasMore` was added to those two so one field answers “am I finished?” on every paged\n  collection; `nextCursor` is unchanged and existing loops keep working.\n\n## Pagination\n\nKeyset, not offset. An offset would let a row written between two requests cross a page\nboundary the caller has already passed: a missing trade, reported as a successful page.\nA short page is not itself a promise that there is nothing more, and on the two resumable\nfeeds, neither is a cursor a promise that there is. Use the column above.\n\nA malformed cursor is refused with `400 invalid_cursor` rather than treated as “from the\nbeginning”: silently replaying an operator’s entire history because of a typo is an expensive\nway to be lenient.",
    "contact": {
      "name": "Parity",
      "url": "/developers"
    }
  },
  "servers": [
    {
      "url": "{host}",
      "description": "There is one deployment, and a key does not encode which host it belongs to. Your integration host is issued to you; configure it as a variable rather than hard-coding it.",
      "variables": {
        "host": {
          "default": "https://api.parity.example"
        }
      }
    }
  ],
  "security": [
    {
      "operatorKey": []
    }
  ],
  "tags": [
    {
      "name": "Catalogue",
      "description": "Events, outcomes and categories. Scope `markets:read`."
    },
    {
      "name": "Trading",
      "description": "Quotes and orders. Scopes `quotes:write` / `orders:write` / `orders:read`."
    },
    {
      "name": "Positions",
      "description": "What a customer holds. Scope `positions:read`."
    },
    {
      "name": "Settlement",
      "description": "What resolved, and what it pays. Scope `settlements:read`."
    },
    {
      "name": "Reconciliation",
      "description": "The verdict and the fact stream. Scope `ledger:read`."
    },
    {
      "name": "Embed",
      "description": "Trade an API key for a short-lived token a BROWSER may hold. Requires `markets:read`, `quotes:write` and `orders:write` together. The three things an embed session can do."
    },
    {
      "name": "Sandbox",
      "description": "Endpoints that model a Parity-held balance. `403 sandbox_only` in live mode; not the production integration path."
    }
  ],
  "paths": {
    "/api/v1/events": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listEvents",
        "summary": "The partner catalogue, cursor-paged",
        "description": "Narrowed to the operator’s offering policy, so a category they are not offered never appears. Contract ids are opaque `ctr_…` and name no venue; artwork is served from Parity’s own origin. A filter with an unrecognised value is a **400**, not a silent full catalogue: an integrator who ships `category=sport` and gets everything back has a bug that looks like working software.",
        "security": [
          {
            "operatorKey": [
              "markets:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "From `GET /api/v1/categories`. An unknown value is `400 invalid_category`."
          },
          {
            "name": "sport",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Narrows INSIDE sports and does not replace `category`. A slug the venue publishes, such as `nfl`, `soccer`, `tennis`. Family slugs roll up: `mma` matches every member of that family in one indexed scan. An unknown value is `400 invalid_sport`, and the error lists the sports YOUR offering makes available, since they differ per operator."
          },
          {
            "name": "league",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Narrows inside `sport`. A league slug the venue publishes, such as `epl`. It cannot contradict the sport: the derivation refuses a league whose own sport disagrees, so `soccer` plus `epl` is always a narrowing and never an empty intersection."
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "trending",
                "volume",
                "closing-soon",
                "new"
              ]
            },
            "description": "`volume` orders by CONTRACTS traded at the venue. A count, not a cash figure. `trending` uses the 24h count."
          },
          {
            "name": "dir",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tradable",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Only events carrying a contract this payload can publish an executable price for. Excludes the five-minute crypto contracts, which always publish `null` executable prices and are quoted from the live book instead. Omit this filter if you want them."
          },
          {
            "name": "closesAfter",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "closesBefore",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque, and checked against the ORDERING IT WAS MINTED UNDER. A token from `sort=volume` replayed against `sort=new` is refused rather than returning a confidently wrong slice."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "description": "Events under `data`, pagination NESTED under `pagination`: the only collection on this surface that nests it. See “Response envelopes” in the API description for the full table.",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "limit",
                        "total",
                        "nextCursor",
                        "hasMore"
                      ],
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer",
                          "description": "Size of the whole filtered set, not what remains after the cursor."
                        },
                        "nextCursor": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "`null` at the end. A FULL page always mints a cursor, so the last page is whichever comes back short: discovering the end can cost one empty request, which is the price of not asking the database a second question on every page."
                        },
                        "hasMore": {
                          "type": "boolean",
                          "description": "Exactly `nextCursor !== null`."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A filter value was not recognised.\n\nCodes: `invalid_category`, `invalid_sort`, `invalid_dir`, `invalid_limit`, `invalid_date`, `invalid_tradable`, `invalid_cursor`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/events/{id}": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "getEvent",
        "summary": "One event, with every outcome",
        "description": "The list caps outcomes per event for card-sized payloads; this returns the whole set, which is what a ladder or a full field needs. `{id}` is an event id or its slug: the two are the same identity wearing different clothes. An event outside the operator’s offering reads as `unknown_event`: the detail endpoint answers for their catalogue, and their catalogue does not contain it.",
        "security": [
          {
            "operatorKey": [
              "markets:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Event id (`evt_…`) or slug."
          }
        ],
        "responses": {
          "200": {
            "description": "The event, WRAPPED IN `data`. `{ \"data\": { … } }`. Never the event object at the top level. Stated this precisely because the shape was not derivable from the document and an integrator had to discover it by calling.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Event"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such event in this operator’s catalogue.\n\nCodes: `unknown_event`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listCategories",
        "summary": "The values `?category=` accepts, with live counts",
        "description": "Counted over EVENTS by the same predicate the list endpoint filters with, so a tab that says 41 returns 41. Only categories that currently have something in them are returned: an integrator building a nav should not discover which tabs are empty by opening all of them. Ordered stably rather than by count, so a nav does not reshuffle when volume moves.",
        "security": [
          {
            "operatorKey": [
              "markets:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Non-empty categories, in a stable order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "totalEvents"
                  ],
                  "description": "Unpaged: the whole list, every call. Categories under `data`.",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    },
                    "totalEvents": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/images/{id}": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "getEventImage",
        "summary": "Event artwork, from Parity’s own origin",
        "description": "THE ONE UNAUTHENTICATED ROUTE on this surface, because it is a URL a browser loads from an `<img>` tag. The ingested artwork URL points at the sourcing venue’s storage bucket, and the bucket is named after the venue, which would undo every other scrub in a hostname and make a partner hotlink a third party’s CDN on their own users’ page loads. Deliberately not a redirect: a 302 puts that hostname straight back into the network panel.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The EVENT id, from the catalogue. A URL is never accepted here. That would make this an open proxy."
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No such event, no usable artwork, or the upstream did not answer with an image."
          }
        }
      }
    },
    "/api/v1/stream/prices": {
      "get": {
        "tags": [
          "Live prices"
        ],
        "operationId": "streamPrices",
        "summary": "Live prices for an embedded surface, over Server-Sent Events",
        "description": "One connection per page, however many contracts it watches. Prices arrive when they move,\nnot on a timer, and, the part a poll cannot do at all, a frame arrives when a price ages\nout of tradability without anything new having happened.\n\n**Fan-out is ours, not yours.** Parity multiplexes the venue side: a thousand browsers\nwatching one contract cost ONE upstream subscription, not a thousand. You do not need to\nproxy this behind your own server to protect anything.\n\n**No API key, ever.** A browser cannot hold one: anything shipped to the page to\nauthenticate the connection is readable by the player and by everything else running on\nit. So every frame is built from opaque `ctr_…` ids, normalised prices and an age: no\nvenue is named, no venue id is published, nothing is user-specific, and the connection\nneeds no secret to be safe.\n\n**The embed session token is optional here, and it decides one thing: whose offering\npolicy applies.** Send `Authorization: Embed <token>` (with `Accept: text/event-stream`)\nand the connection is filtered to what YOUR operator is offered: a contract you may not\noffer is refused in `hello` and no price for it is ever delivered. That is filtered\nBEFORE delivery, not hidden in the UI: a denied market that reached the browser greyed\nout is still in the network panel. Send nothing and you get the unrestricted public\ncatalogue, which is what `/predictions` already is.\n\nA credential that is PRESENTED AND REFUSED: expired, revoked, tampered, wrong origin, \nends the connection with that refusal. It is never downgraded to an anonymous one, which\nwould serve a revoked session MORE than it could see while valid, because \"anonymous\"\nmeans \"no tenant\" and therefore \"no offering filter\". A token in `?token=` is refused\noutright rather than treated as absent.\n\n**Frames.** `hello` (what you are subscribed to, what was refused and why), `snapshot`\n(everything you hold (safe to replace state with), `update` (only what changed) patch,\nnever replace), `heartbeat` (feed age and whether the feed itself has gone quiet),\n`status` (the feed went stale, or recovered), `revoked` (contracts withdrawn from a live\nconnection). Each frame carries `id:`, which is a Parity-wide revision; `EventSource`\nreplays it as `Last-Event-ID` on reconnect and you receive only what changed since, or a\nfresh `snapshot` with `gap: true` where that cannot be honoured.\n\n**Never render a price without reading `tradeable`.** It is false past the age at which\nthe router refuses to quote, and false when no executable side is published. The price is\nstill delivered, because the last known price is real information: it is delivered\nlabelled.",
        "security": [],
        "parameters": [
          {
            "name": "contracts",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated opaque `ctr_…` ids. Bounded per connection; anything past the ceiling is named in `hello.rejected` rather than silently dropped. An internal venue-qualified id is refused, not resolved."
          }
        ],
        "responses": {
          "200": {
            "description": "An open event stream.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "No usable contract ids. Codes: `no_contracts`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "This instance is at its live-connection limit; `Retry-After` says when to come back. Codes: `stream_capacity`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/embed-sessions": {
      "post": {
        "tags": [
          "Embed"
        ],
        "operationId": "createEmbedSession",
        "summary": "Exchange an API key for a browser-safe session",
        "description": "Call this from YOUR SERVER, with your ordinary API credential, for a customer you have\nalready signed in. What comes back is a token your page may hold: scoped to one operator\nand one customer, valid for minutes, and carrying no secret of any kind.\n\n**It is a handle, not a claims token.** Nothing is encoded in it: not your operator id, not\nyour id for the customer, not a scope. Everything it authorises is a row on our side, which\nis what makes it revocable and what keeps your identifiers out of browser history,\npostMessage payloads and referrer headers.\n\n**Send it as `Authorization: Embed <token>`** on the `/api/embed/*` routes. Never in a\ncookie: a cookie rides along on cross-site requests and turns any page on the internet\ninto an order form. Never in a query string: a fifteen-minute credential still has fifteen\nminutes of life in an access log. Both are refused rather than merely discouraged, and\nthere is deliberately no ready-made URL in this response for the same reason.\n\n**Bind the origin.** `returnOrigin` is the page the embed will be framed in. A session\nwithout it works from anywhere the token reaches.\n\n**Your offering policy still governs everything the embed shows and trades.** It is the\nsame policy, the same evaluator and the same refusals as the partner surface: a market you\ndo not carry never appears, cannot be priced, and cannot be ordered by supplying its id.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmbedSessionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "A session. The token is returned once and cannot be recovered afterwards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbedSession"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, an origin that could never match a browser Origin header, or a region we do not know.\n\nCodes: `invalid_json`, `invalid_request`, `invalid_origin`, `invalid_region`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An embed session can browse, quote and order, so the key that mints one must hold all three of those scopes. The refusal names the ones missing.\n\nCodes: `insufficient_scope`, `operator_suspended`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Embed sessions are not configured on this deployment. Ours to fix, not yours.\n\nCodes: `embed_unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quotes": {
      "post": {
        "tags": [
          "Trading"
        ],
        "operationId": "createQuote",
        "summary": "Price a trade, and hold that price",
        "description": "A quote binds contract, side, price, fee and quantity together for a fixed window, and is\nwhat the order is checked against, which is what makes “the screen said 71¢”\nanswerable.\n\n**Sizing.** Exactly one of `stake` or `contracts`. A BUY is sized in CASH: pricing a buy\nfrom a quantity commits the player to whatever that quantity costs at fill time, which is\nthe open-ended exposure the stake-first model exists to prevent. A SELL is sized in\nCONTRACTS, because a cash-sized exit cannot express “close out”: inverting the fee\nrounding either oversells or strands a fraction.\n\n**The fee comes off the top.** A $50 stake at 120bps wagers $49.40. So the gross cash the\nplayer commits IS `authorizationAmount`, and a fully filled order releases nothing.\n\n**A stale price is a 409, not a 400.** The payload was correct and the world moved; the\nright response is to re-quote in a moment, not to fix anything.",
        "security": [
          {
            "operatorKey": [
              "quotes:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "X-Parity-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The OPERATOR'S own id for the end user. Parity never authenticates end users; this opaque string is the entire record of one. A user is created on first use by a write path and is never conjured by a read."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A quote, valid until `expiresAt`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, or no `X-Parity-User`.\n\nCodes: `invalid_json`, `invalid_request`, `missing_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A POLICY REFUSAL. The market is real and priceable; something the operator configured says no.\n\n`market_not_offered`. This operator’s offering policy does not include this market. The id\nfrom our own catalogue was never wrong, so this is not a 404.\n\n`over_wager_limit`. THE STAKE IS ABOVE A MAXIMUM WAGER IN FORCE FOR THIS ACCOUNT. The body\ncarries `maxStake`, in the same major units the request used, on the same field name\n`stake_exceeds_stale_limit` already publishes. Send a smaller stake and it works immediately.\nThe ceiling is whichever is lower of the number the operator set for every account under it\nand a number set against this one account.\n\n`over_period_limit`. THIS ACCOUNT HAS STAKED ITS ALLOWANCE FOR A ROLLING WINDOW. Distinct\nfrom `over_wager_limit` because the answer is the opposite one: the stake is not too large,\nand a smaller one may not help either. The allowance refills with time, so wait rather than\nre-price. `detail` names what has been staked, over what window, and against which ceiling.\n\nBoth limit codes are 403 and not 409 deliberately. 409 on this route means \"re-quote in a\nmoment\" and every other code carrying it clears in seconds; an allowance clears in hours, so\na 409 would invite a retry loop no retry can satisfy.\n\nCodes: `market_not_offered`, `over_wager_limit`, `over_period_limit`, `insufficient_scope`, `operator_suspended`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such contract.\n\nCodes: `market_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The state moved, not the payload. `price_stale`: the price behind the quote is older than the router will trade on. `market_paused`: a five-minute contract with no executable book this instant. `line_not_available`: an outcome on a five-minute contract is executable above 95c, so the line is not wagerable. The body carries `maxCents`. `price_unverified`: the venue publishes no executable book for this contract, so there is no price to stand behind at any stake. All four clear by themselves; re-quote in a moment.\n\nCodes: `price_stale`, `market_paused`, `line_not_available`, `price_unverified`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request is well-formed and cannot be priced. `stake_exceeds_stale_limit` carries the cap to come back under.\n\nCodes: `market_not_open`, `no_price`, `invalid_stake`, `stake_exceeds_stale_limit`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders": {
      "post": {
        "tags": [
          "Trading"
        ],
        "operationId": "submitOrder",
        "summary": "Spend a quote. Idempotent on `clientOrderId`",
        "description": "`201` when this call created the order; `200` with `idempotentReplay: true` when it\nreplayed one that already existed. A quote can be spent once.\n\n**On a BUY** the response carries `operatorMoney`: debit `actualDebit`, release\n`releaseAmount`. On a **partial fill** the release is non-zero and an operator that\ndebits the whole stake instead is keeping money that is not theirs.\n\n**On a SELL** `operatorMoney` is `null` and `operatorExitMoney` is populated: credit\n`sellProceeds`, which is ALREADY NET of the fees reported beside it. Subtracting them\nagain is the double-charge that shape exists to prevent.\n\n**`409 reconciliation_required` is not a rejection.** See the order-lifecycle note in the\nAPI description. Hold the authorization, re-read the order by your own `clientOrderId`,\nand resolve from `settlementState`.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "X-Parity-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The OPERATOR'S own id for the end user. Parity never authenticates end users; this opaque string is the entire record of one. A user is created on first use by a write path and is never conjured by a read."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A replay: this `clientOrderId` already had an order. `idempotentReplay` is `true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "201": {
            "description": "The order was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, or no `X-Parity-User`.\n\nCodes: `invalid_json`, `invalid_request`, `missing_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The quote belongs to another user, or something the operator configured refuses the order.\n\n`market_not_offered`. Policy has disowned the market since the quote was issued.\n\n`over_wager_limit` and `over_period_limit`: the account’s wager limits, RE-CHECKED HERE and\nnot trusted from quote time. A quote stands for up to thirty seconds and a limit written\ninside that window binds on the next order, not thirty seconds later, so a quote issued\nbefore a limit existed is refused when it is spent. The two mean what they mean on\n`POST /api/v1/quotes`: the first is fixed by sending less, the second by waiting.\n\nThese are a separate pair from `over_order_limit`, which is the operator’s tenant-wide\nceiling on one order’s notional and covers sells as well. Keeping them apart is deliberate:\n\"your account’s ceiling\" and \"this player’s limit\" are changed on different screens, and an\noperator answering their own customer needs to know which one spoke.\n\nCodes: `quote_not_yours`, `market_not_offered`, `over_order_limit`, `over_wager_limit`, `over_period_limit`, `insufficient_scope`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such quote.\n\nCodes: `quote_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The payload was correct and the state moved, or the venue’s answer never arrived.\n\n**`quote_already_used`**: this quote was spent by an order that is not the one you are\nsending now. It CANNOT be the same `clientOrderId`: a resend of that id replays as a\n`200` and never reaches this branch. So the body names the collision instead:\n`orderId` is the order that spent the quote, readable with `GET /api/v1/orders/{id}`.\nThat is the deterministic recovery: your own `clientOrderId` looks up an empty page\nhere, because the order was opened under a different one. Then quote again for the\nintent that is still unfilled; never resend a spent quote.\n\n**`quote_expired`** / **`price_stale`**: re-quote and let the player confirm the new\nnumber. Never silently re-price on their behalf.\n\n**`reconciliation_required`** is an UNKNOWN, not a failure. Collateral stays committed\nand the authorization stays held. Re-read the order by your own `clientOrderId` and\nresolve from `settlementState`; releasing here strands a live position with no cash\nbehind it.\n\n**`line_not_available`**: an outcome on this five-minute contract is executable above\n95c, so it is not wagerable. It is not a suspension: the verdict is recomputed from the\nbook on every request, and the line accepts orders again as soon as the book moves back\ninside the band. Re-quote rather than retrying the spent one.\n\nCodes: `quote_expired`, `quote_already_used`, `price_stale`, `reconciliation_required`,\n`line_not_available`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderConflict"
                }
              }
            }
          },
          "422": {
            "description": "The order cannot be placed as asked.\n\n**`insufficient_funds`** is THE CUSTOMER’s balance and **`insufficient_collateral`** is\nYOUR float at the venue. Do not collapse them: one is your customer’s money and the\nother is yours, and the remedies are opposite. Nothing was sent to a venue and no\ncollateral was committed in either case.\n\n`insufficient_funds` is raised only for a tenant whose customer wallets Parity holds,\nand for those tenants it IS A RESERVE. A placed order holds its stake against the\nbalance until the order fills, is rejected or is cancelled, so a customer with $120\nwho has $60 working may place $60 more and no more. Cumulative exposure across a\ncustomer’s open orders never exceeds their balance, whether the orders arrive\ntogether or hours apart, and the reserve is taken in the same transaction as the order\nso a refused order holds nothing. A rejection, a cancellation and the unfilled\nremainder of a partial fill all return it; a fill spends it.\n\nIf you hold your customers’ balances yourself you will never see this code, because\nParity keeps no balance for you to be short of. The check is then yours: debit or\nreserve on your side, and do not read the absence of this error as a reservation Parity\nis keeping for you.\n\nCodes: `market_unavailable`, `no_eligible_venue`, `insufficient_position`, `insufficient_funds`, `insufficient_collateral`, `rejected`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "listOrders",
        "summary": "Order history, newest first",
        "description": "`clientOrderId` lives here rather than on a separate route because looking one up is a FILTER, not a second id space, so a reconciler uses one call for “this one order” and for “everything since 09:00”. It is only unique WITHIN a user, which is what the idempotency index guarantees, so it must be sent with `externalUserId`; returning the first match would silently pick one.",
        "security": [
          {
            "operatorKey": [
              "orders:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "externalUserId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "YOUR own id for one customer. Parity stores no other identifier for them."
          },
          {
            "name": "clientOrderId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "THE RECOVERY IDENTIFIER. Yours, chosen before the request was sent. Requires `externalUserId`."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "submitted",
                "partially_filled",
                "filled",
                "cancelled",
                "rejected"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque. Echo `nextCursor` from the previous page. A malformed cursor is refused rather than treated as \"from the beginning\": silently replaying a whole history because of a typo is an expensive way to be lenient."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "orders",
                    "count",
                    "nextCursor",
                    "hasMore"
                  ],
                  "description": "Orders under `orders`, pagination at the TOP LEVEL: not nested under `pagination` the way `GET /events` does it.",
                  "properties": {
                    "orders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrderDetail"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Rows in THIS page. Not a total. This surface publishes no count of the whole collection."
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "`null` means the collection is exhausted, exactly: the page is cut from a `limit + 1` read, so a null cursor never costs a wasted request to confirm."
                    },
                    "hasMore": {
                      "type": "boolean",
                      "description": "`nextCursor !== null`. Published so one field answers “am I finished?” on every paged collection; `nextCursor` is unchanged and an existing loop on it keeps working."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`clientOrderId` without `externalUserId`, or a cursor this feed cannot decode.\n\nCodes: `invalid_request`, `invalid_cursor`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders/{id}": {
      "get": {
        "tags": [
          "Trading"
        ],
        "operationId": "getOrder",
        "summary": "Read one order back. THE RECOVERY PATH",
        "description": "It must answer for an order that is not finished. `submitted` is the state an unresolved\nvenue answer leaves behind, and this route reports it honestly: no fill,\n`actualDebit` of `0.00`, the whole authorization still showing as `releaseAmount`, and\n`settlementState: \"pending\"`.\n\n`settlementState` collapses six statuses to the only question a wallet has to answer\nright now: `settled` (apply the money and move on) or `pending` (HOLD the\nauthorization, poll, do not release). `pending` and `submitted` are both `pending`\nbecause the correct action is identical and the incorrect one is identically expensive.\n\nA partner who has their own `clientOrderId` but never received ours uses\n`GET /api/v1/orders?externalUserId=&clientOrderId=` instead: that identifier survives a\ncrash mid-request and ours does not.",
        "security": [
          {
            "operatorKey": [
              "orders:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Parity’s `orderId`."
          },
          {
            "name": "fills",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Include the individual executions. Off by default: a fill carries no money contract of its own (the fee and the debit are decided per ORDER), and a partner booking per fill double-counts the moment a venue fills one order in two pieces."
          }
        ],
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderDetail"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such order for this operator. Another partner’s order id gives the same answer as one that never existed. Distinguishing them would make this route an oracle for guessed ids.\n\nCodes: `unknown_order`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders/{id}/confirm-reserve": {
      "post": {
        "tags": [
          "Trading"
        ],
        "operationId": "confirmOrderReserve",
        "summary": "Confirm the customer’s funds are held. The order becomes executable",
        "description": "`POST /api/v1/orders` records a real order and reserves against it, and stops. The order\nis `reserve_pending`: recorded, transmitted nowhere, and NOT EXECUTABLE. This call is\nwhat makes it executable, after you have placed the hold on your own side.\n\nThe ordering matters in one direction only. Parity will never transmit an order whose\nreserve you have not confirmed: `reserve_pending` has no path to a venue at all. The\norder becomes `pending_venue_execution`, cleared to execute, and it progresses to\n`submitted → filled` under the state names you already consume.\n\n**IDEMPOTENT.** A confirmation that timed out is safe to resend: the same call twice\nyields ONE executable order and a `200`, never a conflict. The alternative recovery is a\nread: `GET /api/v1/orders?externalUserId=&clientOrderId=` reports `reserve_pending` or\n`pending_venue_execution` and there is no third answer. Neither route asks you to\nremember anything we do not already hold.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Parity’s `orderId`."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmReserveRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The reserve is confirmed. `idempotentReplay` is `true` when it already was.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderDetail"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body.\n\nCodes: `invalid_json`, `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No recorded order intent with this id for this operator.\n\nCodes: `unknown_order`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The order is not awaiting a reserve. It was cancelled, or it is already at a venue, filled or rejected. Absorbing this would tell you your hold stands behind a live order when it stands behind nothing.\n\nCodes: `order_cancelled`, `not_an_intent`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders/{id}/cancel": {
      "post": {
        "tags": [
          "Trading"
        ],
        "operationId": "cancelOrder",
        "summary": "Withdraw an order that was never transmitted. Returns a release instruction",
        "description": "Returns a RELEASE INSTRUCTION: an id, a currency and an exact amount to hand back, with\nan `actualDebit` of `0.00` so `authorizationAmount = actualDebit + releaseAmount` closes\nhere exactly as it does on a fill. Apply it and you are finished; there is nothing to\nrecompute.\n\n**EXACTLY ONCE.** A second call returns `200` with the SAME `cancellationId` and the\nSAME `releaseAmount` and `idempotentReplay: true`. A cancellation whose response you\nnever saw is safe to resend, and cannot credit a customer twice.\n\n**It refuses any order whose `transmissionState` is not `not_transmitted`**, and\n`reconciliation_required` above all. That is the state where Parity asked a venue and\ngot no answer, and a release issued against one would hand a customer back money\nstanding behind a position that may really exist. The `409` carries\n`transmissionState: \"unknown\"`, which says the right thing: keep holding the\nauthorization, and wait for reconciliation.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Parity’s `orderId`."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelOrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled. The cancellation object carries the release instruction to apply.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderCancellation"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body.\n\nCodes: `invalid_json`, `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No recorded order intent with this id for this operator.\n\nCodes: `unknown_order`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This order’s `transmissionState` is not `not_transmitted`, so it cannot be withdrawn. Read `transmissionState`: `unknown` means keep holding the authorization; `resolved` means the order’s own `operatorMoney` already says what to apply.\n\nCodes: `not_cancellable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/positions": {
      "get": {
        "tags": [
          "Positions"
        ],
        "operationId": "listPositions",
        "summary": "What an operator’s customers currently hold",
        "description": "The answer to “what is still open on our book?”, which no write path can give.\n\n`operatorMoney` carries `costBasis` and `realisedPnl` and NOTHING TO APPLY. A position is\na statement of fact; the instructions to move money live where they are generated: on\nthe order and on the settlement. A position that also carried an amount would give an\noperator two places to book the same dollar from.\n\nThere is no mark-to-market. Valuing an open position against the current bid would make\nthis response change between two identical calls because a feed moved, and a number that\nmoves on its own cannot be reconciled against. A partner who wants the exit value quotes\na sell: a real price, not an estimate.\n\nKeyset-paged on `openedAt`, which never moves. `updatedAt` would be more useful for\npolling and is unusable as a cursor key: a position sold down mid-page would jump to the\nfront and the row it displaced would never be returned.",
        "security": [
          {
            "operatorKey": [
              "positions:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "externalUserId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "YOUR own id for one customer. Parity stores no other identifier for them."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "closed",
                "settled"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque. Echo `nextCursor` from the previous page. A malformed cursor is refused rather than treated as \"from the beginning\": silently replaying a whole history because of a typo is an expensive way to be lenient."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of positions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "positions",
                    "count",
                    "nextCursor",
                    "hasMore"
                  ],
                  "description": "Positions under `positions`, pagination at the top level. Same envelope as `GET /orders`.",
                  "properties": {
                    "positions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Position"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Rows in THIS page."
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "`null` means exhausted, exactly. Cut from a `limit + 1` read."
                    },
                    "hasMore": {
                      "type": "boolean",
                      "description": "`nextCursor !== null`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A cursor this feed cannot decode.\n\nCodes: `invalid_cursor`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/positions/{id}": {
      "get": {
        "tags": [
          "Positions"
        ],
        "operationId": "getPosition",
        "summary": "One position",
        "description": "A settlement webhook and the settlement feed both key on `positionId`, so a partner processing one has an id and one question: what was this? Making them page a list to answer it would mean walking a growing feed to find a row they can already name.",
        "security": [
          {
            "operatorKey": [
              "positions:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The position.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Position"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such position for this operator.\n\nCodes: `unknown_position`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/settlements": {
      "get": {
        "tags": [
          "Settlement"
        ],
        "operationId": "listSettlements",
        "summary": "The settlement register. What settled, and what it paid",
        "description": "The record an operator queries to reconcile a period: filterable by customer, date and\noutcome, keyset-paged on a monotonic sequence, and safe to re-read.\n\nEvery record carries `settlementCredit` and `voidCredit` as exact decimal strings, and\nexactly one of them is non-zero (a loss zeroes both). They are two names for one payout\nbecause they are different instructions: winnings against a wager, versus a refund that\nreverses one. Operators book those differently.\n\n`realisedPnl` is THIS settlement’s figure, not the position’s running total: they differ\nwhenever the position was partly exited before it resolved. `lifetimeRealisedPnl` carries\nthe other one, named for what it is.\n\nAn unknown `externalUserId` is a **404**, not an empty page: `[]` reads as “this customer\nhas settled nothing”, and an operator will believe it until a credit goes missing.",
        "security": [
          {
            "operatorKey": [
              "settlements:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "externalUserId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "YOUR own id for one customer. Parity stores no other identifier for them."
          },
          {
            "name": "outcome",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "won",
                "lost",
                "void"
              ]
            }
          },
          {
            "name": "settledFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Inclusive."
          },
          {
            "name": "settledTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Inclusive."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque. Echo `nextCursor` from the previous page. A malformed cursor is refused rather than treated as \"from the beginning\": silently replaying a whole history because of a typo is an expensive way to be lenient."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of settlement records.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "settlements",
                    "nextCursor",
                    "hasMore"
                  ],
                  "description": "Records under `settlements`, pagination at the top level. There is NO `count` here, unlike `GET /orders` and `GET /positions`.",
                  "properties": {
                    "settlements": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Settlement"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A RESUMABLE WATERMARK, not an end marker: it stays non-null on the last page so a poller can send it back and pick up whatever settles next. `while (nextCursor)` never terminates here."
                    },
                    "hasMore": {
                      "type": "boolean",
                      "description": "**THE TERMINATOR.** `false` means this page reached the tail. Stop on this, never on the cursor."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A filter or cursor was malformed.\n\nCodes: `invalid_limit`, `invalid_settled_from`, `invalid_settled_to`, `invalid_cursor`, `invalid_outcome`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such customer for this operator.\n\nCodes: `unknown_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/settlements/positions": {
      "get": {
        "tags": [
          "Settlement"
        ],
        "operationId": "listPositionSettlements",
        "summary": "Legacy position-settlement feed. Superseded",
        "deprecated": true,
        "description": "Kept working because it is already a published contract, and superseded by\n`GET /api/v1/settlements`, which returns the same records with a gap-free keyset cursor.\nThe response says so in `supersededBy`.\n\nThe reason to move: this pages on `settledAt` with no tiebreaker, and settlements written\nin one transaction share a timestamp exactly, so a poll can drop or repeat rows.\n\nA malformed `since` is refused rather than treated as “from the beginning”: silently\nreplaying every settlement an operator has ever had, because of a typo, is a very\nexpensive way to be lenient.\n\nThis feed used to publish `venueListingId`, which is `${provider}:${providerMarketId}`:\nthe name of the source and the source’s own identifier for the contract. It no longer\ndoes. Every record addresses the contract by the opaque `ctr_…` id, so **no response on\nthis surface names a venue**. If you stored the old field, stop: it is not an identifier\nParity will honour, and `contractId` is.",
        "security": [
          {
            "operatorKey": [
              "settlements:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The `cursor` from the previous poll."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settlement facts, ascending by `settledAt`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "settlements",
                    "cursor",
                    "count",
                    "supersededBy"
                  ],
                  "description": "The odd one out, and left that way because it is already published: the pagination field is `cursor`, NOT `nextCursor`, and it is passed back as `?since=` rather than as `?cursor=`.",
                  "properties": {
                    "settlements": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PositionSettlement"
                      }
                    },
                    "cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Pass back as `?since=`. The `settledAt` of the last row, so it is a timestamp rather than an opaque token."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Rows in THIS page."
                    },
                    "supersededBy": {
                      "type": "string",
                      "examples": [
                        "/api/v1/settlements"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`since` was not an ISO-8601 timestamp.\n\nCodes: `invalid_since`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reconciliation": {
      "get": {
        "tags": [
          "Reconciliation"
        ],
        "operationId": "getReconciliationReport",
        "summary": "The verdict: do Parity, the cash ledger and the venue agree?",
        "description": "Scoped to the calling operator at the query level. Findings are externalised at the boundary: `subjectType: \"user\"` means `subjectId` is YOUR id for a customer, and `subjectType: \"record\"` means it is a Parity id you can quote back to us. `ok: false` means at least one finding needs a human.",
        "security": [
          {
            "operatorKey": [
              "ledger:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Defaults to the accounting epoch, before which trades legitimately predate the ledger."
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationReport"
                }
              }
            }
          },
          "400": {
            "description": "`from` was not an ISO-8601 timestamp.\n\nCodes: `invalid_from`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reconciliation/feed": {
      "get": {
        "tags": [
          "Reconciliation"
        ],
        "operationId": "readReconciliationFeed",
        "summary": "Every money fact, in one totally ordered, resumable stream",
        "description": "Resumption is the whole design. Store `nextCursor`, send it back, and you receive every\nfact recorded since and nothing you have already seen. **Reading does not consume**: the\nsame cursor returns the same items forever, so a consumer that crashes mid-batch simply\nre-reads. Advance your stored cursor only once the batch is applied.\n\n`hasMore: false` means caught up. Poll again later with the same cursor. Caught up means\n“everything that is provably final”, not “everything that exists this instant”: the head\nof the stream waits for in-flight writes to land, so a row may arrive a moment later than\nit committed and will never fail to arrive.\n\nAn unrecognised `type` is refused rather than ignored. Silently dropping it returns a\nfeed that looks complete and is not.",
        "security": [
          {
            "operatorKey": [
              "ledger:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque. Echo `nextCursor` from the previous page. A malformed cursor is refused rather than treated as \"from the beginning\": silently replaying a whole history because of a typo is an expensive way to be lenient."
          },
          {
            "name": "type",
            "in": "query",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "order.state_changed",
                  "order.filled",
                  "fee.assessed",
                  "collateral.moved",
                  "ledger.posted",
                  "position.exited",
                  "position.settled",
                  "webhook.emitted"
                ]
              }
            },
            "description": "Repeatable, or comma-separated."
          },
          {
            "name": "externalUserId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "YOUR own id for one customer. Parity stores no other identifier for them."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of facts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor",
                    "hasMore"
                  ],
                  "description": "Facts under `items`. Not `data`, and not a name taken from the resource. Pagination at the top level.",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedItem"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A resumable watermark, like `GET /settlements`: non-null at the tail, because the whole point is to send it back later and receive whatever has landed since."
                    },
                    "hasMore": {
                      "type": "boolean",
                      "description": "**THE TERMINATOR.** `false` means caught up. Poll again later with the SAME cursor."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A cursor, limit or type was not recognised.\n\nCodes: `invalid_cursor`, `invalid_limit`, `invalid_type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such customer for this operator.\n\nCodes: `unknown_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/balance": {
      "get": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "getSandboxBalance",
        "summary": "A Parity-held balance. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.** This models a per-user balance held by Parity,\nwhich a live operator does not have: in production the OPERATOR owns its customers’ cash\nledger and Parity returns instructions against it. It exists so that somebody learning\nthe flow has simulated money to spend and somewhere to watch it move.\n\nWithin the sandbox: `cash` is `available + reserved` and nothing else. `positionCost` is\nwhat the customer’s open contracts COST and is reported beside them, never summed with\nthem: a position is worth whatever the venue says today and cannot be spent.\n\nEvery figure is derived by summing ledger lines at read time. There is no stored balance\ncolumn to drift.",
        "security": [
          {
            "operatorKey": [
              "ledger:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "YOUR id for the customer. The same value you send as `X-Parity-User`."
          },
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "USD"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The derived balance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxBalance"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such customer. A read never creates one: a typo must surface as an error, not as a confident $0.00.\n\nCodes: `unknown_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/ledger": {
      "get": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "listSandboxMovements",
        "summary": "The statement behind that balance. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.** See the balance endpoint.\n\nAppend-only: nothing is ever edited or removed, and a correction appears as its own\n`ADJUSTMENT` row carrying `correctsEntryId`, so the wrong number and the entry that fixed\nit both stay visible.\n\n`amount` is always positive; the DIRECTION lives in `type`. Switch on the movement rather\nthan inferring intent from a sign: a `FEE` and a `WITHDRAWAL` both reduce cash and mean\nentirely different things.\n\nThis feed publishes EVERY entry, so treat `type` as open: handle the values you care\nabout and pass anything else through unchanged rather than rejecting it. `HOSTED_SPEND`,\n`HOSTED_PAYOUT` and `VENUE_FEE` are the three most recent additions, and there will be\nothers. A reader that switches exhaustively on this field will break on a movement that\nwas added for a reason unrelated to it.\n\nThe debit/credit LINES are not exposed: publishing them would make Parity’s chart of\naccounts part of the partner contract, unchangeable without a breaking change.",
        "security": [
          {
            "operatorKey": [
              "ledger:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque. Echo `nextCursor` from the previous page. A malformed cursor is refused rather than treated as \"from the beginning\": silently replaying a whole history because of a typo is an expensive way to be lenient."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of movements, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "userId": {
                      "type": "string"
                    },
                    "movements": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Movement"
                      }
                    },
                    "count": {
                      "type": "integer"
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A cursor this feed cannot decode.\n\nCodes: `invalid_cursor`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such customer for this operator.\n\nCodes: `unknown_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/limits": {
      "get": {
        "tags": [
          "Users"
        ],
        "operationId": "getUserWagerLimits",
        "summary": "One customer's wager limits, and what actually applies",
        "description": "THE RESPONSE CARRIES WHAT APPLIES, NOT ONLY WHAT YOU SET, and that is the field to read.\n\nThere are two rungs. `operator` covers every account under you. `account` is this\ncustomer. The account rung can only ever TIGHTEN the operator rung, never loosen it,\nso an account ceiling set ABOVE the operator ceiling has no effect at all. A caller\nthat read only its own write would believe it had raised a limit it had not raised.\n\n`effective` is what the quote and order seams will actually enforce, with the rung\nthat decided each figure named. A responsible gambling engine should read `effective`\nand nothing else.\n\nAmounts are exact integers of minor units, like the rest of the money surface. `null`\nmeans no opinion at that rung, which is a different fact from `0`, a ceiling of\nnothing that would refuse every wager.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your own id for the customer."
          }
        ],
        "responses": {
          "200": {
            "description": "The account rung, the operator rung, and what applies."
          },
          "400": {
            "description": "`missing_user`."
          },
          "401": {
            "description": "Unrecognised key."
          }
        }
      },
      "put": {
        "tags": [
          "Users"
        ],
        "operationId": "setUserWagerLimits",
        "summary": "Set one customer's wager limits",
        "description": "For a limit that arrives from YOUR system: a cool-off, a self-exclusion, a player-set\ndeposit limit, or a ceiling your risk team applies. Previously these existed only in\nthe Parity admin console, which is not an integration for an operator running\nautomated triggers.\n\n`maxWagerMinor` caps ONE wager. `periodStakeMinor` with `periodHours` caps total stake\nacross a rolling window. The pair travels together: a stake ceiling with no window is\nnot a rule anybody can enforce, and a window with no ceiling bounds nothing, so the\nhalf-written form is refused rather than stored as a limit that never fires.\n\nSend `null` at a field to withdraw the opinion at this rung. The reply is the same\nshape as the GET, so you can see what your write actually changed.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your own id for the customer."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "maxWagerMinor": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Ceiling on ONE wager, in minor units."
                  },
                  "periodStakeMinor": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Ceiling on total stake across the window."
                  },
                  "periodHours": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "The rolling window, in whole hours."
                  },
                  "note": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "Why, for the audit trail. Never shown to the player."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. The body reports the account rung, the operator rung and what applies."
          },
          "400": {
            "description": "`invalid_request`, including the half-written period form."
          },
          "401": {
            "description": "Unrecognised key."
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "operationId": "clearUserWagerLimits",
        "summary": "Clear one customer's account-rung limits",
        "description": "Removes the ACCOUNT rung only. Any operator-wide ceiling above it still stands, which\nis why the reply still carries `effective`: a 200 here does not mean the customer is\nnow unlimited.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your own id for the customer."
          }
        ],
        "responses": {
          "200": {
            "description": "Cleared. The body reports what still applies."
          },
          "400": {
            "description": "`missing_user`."
          },
          "401": {
            "description": "Unrecognised key."
          }
        }
      }
    },
    "/api/v1/users/{id}/jurisdiction": {
      "put": {
        "tags": [
          "Users"
        ],
        "operationId": "assertUserJurisdiction",
        "summary": "State where one of your customers is",
        "description": "WHERE YOU SAY YOUR CUSTOMER IS. You ran the KYC, you hold the licence, so the\nassertion is yours and Parity records and applies it. Parity does not authenticate\nyour end users and does not geolocate them: nothing here is a lookup.\n\nA country as ISO 3166-1 alpha-2, optionally with the part of an ISO 3166-2 code after\nthe hyphen for a country whose sub-national treatment differs. The SHAPE is validated\nand the value is not, because a list of permitted countries would go stale and a\ncountry missing from it would be silently unassertable.\n\nThis is separate from the `region` on an embed session, which ranks a board and whose\nvocabulary includes things that are not countries.\n\n**It changes nothing until you switch jurisdiction enforcement on.** Assertions are\nrecorded either way, so you can populate them, read back how often they disagree with\nthe country we observe, and only then enforce.\n\nAn assertion REPLACES the whole pair rather than merging into it: send the subdivision\nagain, or send null to go back to country level.\n\nStoring this does not make anybody compliant anywhere. It is a fact you supplied,\napplied against a rulebook you wrote.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "country"
                ],
                "properties": {
                  "country": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2,
                    "example": "MT"
                  },
                  "subdivision": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 3,
                    "example": "NY"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored, and echoed back normalised rather than as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "userId": {
                      "type": "string"
                    },
                    "jurisdiction": {
                      "type": "object",
                      "properties": {
                        "country": {
                          "type": "string"
                        },
                        "subdivision": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "assertedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The country was not a two-letter code.\n\nCodes: `invalid_request`, `invalid_jurisdiction`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Users"
        ],
        "operationId": "getUserJurisdiction",
        "summary": "What you told us, and what we observed",
        "description": "Two facts, never collapsed into one. `jurisdiction` is what YOU asserted and is the\nonly thing that decides anything. `observedCountry` is resolved from the player’s own\naddress the last time their browser reached us, and is EVIDENCE: it never grants\naccess and it never refuses on its own, because a traveller, a corporate VPN and a\nmobile carrier all produce a foreign address for a properly onboarded customer.\n\n`mismatch` is the comparison, and it is null when either half is missing: \"we cannot\ncompare\" is not the same answer as \"they agree\".",
        "security": [
          {
            "operatorKey": [
              "markets:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What is on record for this customer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "userId": {
                      "type": "string"
                    },
                    "jurisdiction": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "assertedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "assertedSource": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "observedCountry": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "observedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "mismatch": {
                      "type": [
                        "boolean",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sandbox/scenarios": {
      "post": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "armSandboxScenario",
        "summary": "Arm a deterministic partial fill for your next order. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.**\n\nA clean fill is easy to test against the sandbox; a PARTIAL one is impossible, because it\ndepends on a venue’s depth at a moment nobody controls. That leaves the single case where\n`actualDebit` is smaller than `authorizationAmount`, and the `releaseAmount` you must hand\nback, untested until it happens to a real customer.\n\nArm a scenario, then submit an order as usual. The next order fills partially and returns the\nordinary money contract: `authorizationAmount = actualDebit + releaseAmount`, exactly.\n\nGive **one** of `fillFraction` (0 < f < 1) or `fillAmountMinor` (whole minor units). A\nscenario fires once, expires after an hour, and is scoped to your tenant. Pass `clientOrderId`\nto arm one specific order rather than the next one, which is what you want if your tests\nrun in parallel.\n\nThis is not a second accounting engine. It reduces the stake the simulated venue is asked to\nconsume; every figure after that comes from the same code a real partial fill would use.",
        "security": [
          {
            "operatorKey": [
              "orders:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "partial_fill"
                    ],
                    "default": "partial_fill"
                  },
                  "fillFraction": {
                    "type": "number",
                    "examples": [
                      0.5
                    ]
                  },
                  "fillAmountMinor": {
                    "type": "integer",
                    "examples": [
                      987
                    ]
                  },
                  "clientOrderId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Armed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scenarioId": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string"
                    },
                    "fillFraction": {
                      "type": "number",
                      "nullable": true
                    },
                    "fillAmount": {
                      "type": "string",
                      "nullable": true,
                      "examples": [
                        "9.87"
                      ]
                    },
                    "clientOrderId": {
                      "type": "string",
                      "nullable": true
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The scenario is not a partial fill.\n\nCodes: `scenario_invalid`, `invalid_body`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "listSandboxScenarios",
        "summary": "What you have armed. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.**\n\nPass `?disarm=<scenarioId>` to remove one before listing the rest.",
        "security": [
          {
            "operatorKey": [
              "orders:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "disarm",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Armed, unconsumed, unexpired scenarios.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scenarios": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No armed scenario with that id.\n\nCodes: `scenario_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sandbox/resolutions": {
      "post": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "resolveSandboxPosition",
        "summary": "Settle one of your positions on demand. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.**\n\nA real market resolves when it resolves, which may be months away, so the one case where\nmoney moves TOWARD your customer has been untestable. Resolve a position as `WIN`, `LOSS` or\n`VOID` and the ordinary settlement runs: a settlement record appears on `/api/v1/settlements`\ncarrying an exact `settlementCredit` or `voidCredit`, the reconciliation feed sees it, and\nnothing about the arithmetic differs from a market that resolved by itself.\n\n`WIN` and `LOSS` are relative to the **position’s own side**, so you never have to work out\nthat a NO holder wins on a NO resolution.\n\nIdempotent: asking twice for the same outcome is safe and settles once. Asking for a\nDIFFERENT outcome answers `409` rather than overwriting: a position that was going to win\nand now loses is a contradiction, not a retry.\n\nThe outcome names one POSITION, never a market. Markets are a shared catalogue; resolving one\nwould settle every other operator’s positions on it too.",
        "security": [
          {
            "operatorKey": [
              "settlements:read"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "positionId",
                  "resolution"
                ],
                "properties": {
                  "positionId": {
                    "type": "string"
                  },
                  "resolution": {
                    "type": "string",
                    "enum": [
                      "WIN",
                      "LOSS",
                      "VOID"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded, and settled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "positionId": {
                      "type": "string"
                    },
                    "resolution": {
                      "type": "string"
                    },
                    "alreadyRequested": {
                      "type": "boolean"
                    },
                    "settled": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is not a resolution.\n\nCodes: `invalid_body`, `position_not_open`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such position for this operator.\n\nCodes: `position_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already set to resolve differently.\n\nCodes: `resolution_conflict`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/funding/supported-assets": {
      "get": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "listSupportedAssets",
        "summary": "Fundable asset/chain pairs, read live. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.**\n\nNever cached and never hard-coded, and that is a safety property rather than an\nimplementation detail: a stale allowlist hands a user a deposit address for a route that\nno longer exists, and those funds are not recoverable. So an upstream outage answers\n`502 bridge_unavailable` (an honest “we cannot tell you right now”), and never a\nremembered list presented as current.\n\nBoth funding reads require `funding:write`; there is no funding read scope today.",
        "security": [
          {
            "operatorKey": [
              "funding:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "symbol",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "symbols",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated."
          },
          {
            "name": "chainId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The live route catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SupportedAssets"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The funding bridge could not be reached. No cached list is served.\n\nCodes: `bridge_unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/funding/deposit-address": {
      "post": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "createDepositAddress",
        "summary": "Open a funding route for one customer. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.**\n\nThe route is validated against the LIVE catalogue before anything is persisted, so a\ndeposit row can never describe a pair the bridge does not accept. The row is written\nBEFORE an address is handed out: an address a customer has already funded and we have no\nrecord of is the one failure with no recovery path.\n\nRe-requesting is a LOOKUP, not a new deposit: the same (operator, address, asset, chain)\nreturns the existing deposit, enforced by a unique index. Two rows against one address\nwould each claim whatever arrived at it.\n\n**Sending funds does not credit a balance.** Money arriving is a fact about a blockchain;\nit becomes a balance only when a ledger entry is posted, which is a separate, deliberate\ntransition. Poll `pollUrl`.",
        "security": [
          {
            "operatorKey": [
              "funding:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "X-Parity-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The OPERATOR'S own id for the end user. Parity never authenticates end users; this opaque string is the entire record of one. A user is created on first use by a write path and is never conjured by a read."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "asset",
                  "chainId"
                ],
                "properties": {
                  "asset": {
                    "type": "string",
                    "description": "Token symbol exactly as the bridge spells it."
                  },
                  "chainId": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "integer"
                      }
                    ],
                    "description": "An identifier, not an integer to do arithmetic on. Normalised to a string; some are larger than a JS integer."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The deposit and its instructions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DepositAddress"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, or no `X-Parity-User`.\n\nCodes: `invalid_json`, `invalid_request`, `missing_user`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "That asset/chain pair is not in the live catalogue.\n\nCodes: `route_not_supported`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The bridge could not be reached, so no address was issued. Nothing was recorded; retry safely.\n\nCodes: `bridge_unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No treasury destination is configured, so there is nowhere for a bridged deposit to land.\n\nCodes: `funding_unavailable`, `no_address_for_chain`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/funding/deposits/{id}": {
      "get": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "getDeposit",
        "summary": "Where one deposit has got to. SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.**\n\n```\ncreated → awaiting_funds → detected → bridging ─╬─→ credited\n                                                ╬─→ failed\n        ── a fact about a blockchain ──          ── a fact about OUR books ──\n```\n\nEverything left of that line describes a chain and says nothing about whose balance the\nmoney is. `credited` is the separate decision, reachable ONLY by posting a ledger entry\nin the same transaction that writes the status, so there is no ordering in which a\ncustomer is credited without an entry.\n\nThis is why the bridge reporting `COMPLETED` does not advance us to `credited`: it sets\n`eligibleToCredit`. A bridge that redelivers `COMPLETED` after a retry would otherwise\npay twice.\n\nA failed bridge poll still returns `200` with the stored lifecycle and a `syncError`. A\n`502` here would read to a customer as though the money were gone.",
        "security": [
          {
            "operatorKey": [
              "funding:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "`false` reads the stored state without polling the bridge."
          }
        ],
        "responses": {
          "200": {
            "description": "The deposit, its timeline and every transition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deposit"
                }
              }
            }
          },
          "401": {
            "description": "The key is absent, unrecognised or revoked. Waiting does not help.\n\nCodes: `missing_credentials`, `invalid_key`, `revoked_key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key is real and may not do this. `insufficient_scope` names the scope to add; `sandbox_only` means the endpoint models a Parity-held balance and exists for sandbox integration only.\n\nCodes: `insufficient_scope`, `operator_suspended`, `sandbox_only`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such deposit for this operator. Not-found and not-yours are the same answer on purpose: confirming an id exists is itself a disclosure.\n\nCodes: `unknown_deposit`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-key limit. `retryAfterSeconds` and the `RateLimit-*` headers carry the window.\n\nCodes: `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server failure. Safe to retry a read.\n\nCodes: `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "position.settled": {
      "post": {
        "summary": "A position resolved. NOT YET DELIVERED. Poll the settlement feed.",
        "description": "Both this and `GET /api/v1/settlements` carry the same fact and key on the same `positionId`, so a partner may use either without processing it twice. It carries `externalUserId`, YOUR id for the customer, and `operatorMoney` with exactly one of `settlementCredit` and `voidCredit` non-zero as exact decimal strings: credit that, rather than reading the `payout` float and guessing the currency. `settlementId` is the idempotency key for the credit. Delivery is at-least-once by nature: key on `dedupeKey` to make handling idempotent. The signature is `parity-signature: t=<unix>,v1=<hmac-sha256 of \"t.body\">`, with a 300-second replay window. The same value is also sent as `predicta-signature`, the header this shipped under, so verifying either one is equivalent; the timestamp is inside the signed material precisely so an old signature is refused even when the MAC is perfect. Redirects are never followed. A redirect is a destination we did not verify. The payload is narrowed exactly as the REST surface is: the contract is the opaque `contractId`, and neither the source nor Parity’s internal user uuid appears.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEventPositionSettled"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Any 2xx acknowledges. A 3xx is treated as a failure."
          }
        }
      }
    },
    "order.filled": {
      "post": {
        "summary": "A FILL was recorded on an order. NOT YET DELIVERED.",
        "description": "ONE EVENT PER FILL, not per order. `dedupeKey` is `order.filled:<orderId>:<fillId>`, so an order that fills in pieces raises one event per piece; it used to be keyed on the order alone, which meant the second and every later fill on a real order emitted nothing at all. `fillQuantity` and `fillPrice` describe THIS fill; `filledQuantity` and `averagePrice` are the order’s running totals; and `operatorMoney` is CUMULATIVE FOR THE ORDER, never per fill, because an amount stamped on each fill is an amount an operator can debit twice. Read `transmissionState` before acting on `releaseAmount`: `unknown` means the venue can still fill more of this order and the remainder is not yours to hand back yet.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEventOrderFilled"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "order.rejected": {
      "post": {
        "summary": "An order was rejected, and here is what to release. NOT YET DELIVERED.",
        "description": "It carried no amount at all while the operator was still holding an authorization against the order. Nothing was spent, so `operatorMoney.actualDebit` is `0.00` and `releaseAmount` is the whole authorization. `operatorMoney` is null on a SELL, as it is everywhere else on this API: an exit reserves no cash, so a rejected exit has nothing to hand back.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEventOrderRejected"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "order.intent_recorded": {
      "post": {
        "summary": "A real order was recorded and funded, and sent nowhere. NOT YET DELIVERED.",
        "description": "THE EVENT A LIVE BACK OFFICE ACTUALLY NEEDS. Under `executionIntent: \"record_intent\"` no venue is contacted, so `order.filled` never fires: the order exists, funds are reserved against it, and no venue has heard of it. `authorizationAmount` is an exact decimal string to RESERVE AND HOLD. There is deliberately no `releaseAmount` here, because nothing has been decided yet; `order.cancelled` is the event that instructs a release, and it does so when it is true.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEventOrderIntentRecorded"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "order.cancelled": {
      "post": {
        "summary": "An untransmitted order was withdrawn. NOT YET DELIVERED.",
        "description": "The release instruction, with the same identity as every other money block: `authorizationAmount = actualDebit + releaseAmount`, exactly, and `actualDebit` is always `0.00` because nothing was transmitted. `cancellationId` is the idempotency key for the release.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEventOrderCancelled"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "market.resolution_changed": {
      "post": {
        "summary": "A market’s resolution changed. NOT YET DELIVERED.",
        "description": "It reports that a source changed its mind about something you were already paid on. No money has moved and none will move automatically; `action` is `operator_review_required`. One event per change rather than per sweep. The contract is the opaque `contractId`, and so is the `dedupeKey`, which is published verbatim in the envelope and on `GET /api/v1/reconciliation/feed`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "operatorKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The OPERATOR credential. `pk_test_…` belongs to a sandbox tenant and `sk_live_…` to a live one; the spaces do not overlap and the tenant’s mode (not the prefix) decides what the key can reach. Stored as a SHA-256 hash, shown once at issue."
      }
    },
    "schemas": {
      "EmbedSessionRequest": {
        "type": "object",
        "required": [
          "externalUserId"
        ],
        "properties": {
          "externalUserId": {
            "type": "string",
            "description": "YOUR own id for the customer this session is for. The same value you would send as `X-Parity-User`. Opaque to Parity, and the only user this session can ever act as."
          },
          "returnOrigin": {
            "type": "string",
            "examples": [
              "https://casino.example"
            ],
            "description": "The origin the embed will be framed in. Stored as scheme + host + port; a path is dropped rather than refused, because an `Origin` header never carries one. https only, except on loopback."
          },
          "locale": {
            "type": "string",
            "examples": [
              "pt-BR"
            ],
            "description": "BCP-47. Presentation only."
          },
          "region": {
            "type": "string",
            "examples": [
              "br"
            ],
            "description": "Selects region-scoped offering rules. A routing hook, NOT a legal determination."
          },
          "theme": {
            "type": "object",
            "description": "Presentation config echoed to the embed. Never a permission. At most 32 keys."
          },
          "ttlSeconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 3600,
            "default": 900,
            "description": "Shorter than the default is honoured; longer is refused rather than silently capped, because a partner planning around a longer session needs to know they did not get one."
          }
        }
      },
      "EmbedSession": {
        "type": "object",
        "required": [
          "sessionId",
          "token",
          "expiresAt"
        ],
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "Ours, for support and for revocation."
          },
          "token": {
            "type": "string",
            "examples": [
              "pes1.K1.…"
            ],
            "description": "RETURNED ONCE. Send as `Authorization: Embed <token>`."
          },
          "tokenType": {
            "type": "string",
            "examples": [
              "embed_session"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresInSeconds": {
            "type": "integer"
          },
          "operatorId": {
            "type": "string"
          },
          "externalUserId": {
            "type": "string"
          },
          "origin": {
            "type": [
              "string",
              "null"
            ],
            "description": "The normalised origin the session is bound to, or null if unbound."
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "theme": {
            "type": "object"
          },
          "usage": {
            "type": "string",
            "description": "Where to put the token, said in the response."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Branch on `error`, never on `message`.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable code."
          },
          "message": {
            "type": "string"
          },
          "detail": {
            "description": "Present on validation failures and some refusals."
          },
          "required": {
            "type": "string",
            "description": "On `insufficient_scope`: the scope this key needs."
          },
          "retryAfterSeconds": {
            "type": "number",
            "description": "On `rate_limited`."
          }
        }
      },
      "OrderConflict": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "The 409 body from `POST /api/v1/orders`. An `Error` with one field added: on `quote_already_used` it names the order that already spent the quote, which is the only deterministic way back to it. `clientOrderId` cannot answer, because a resend of yours would have replayed as a 200 instead of arriving here.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "quote_expired",
              "quote_already_used",
              "price_stale",
              "reconciliation_required",
              "line_not_available"
            ]
          },
          "message": {
            "type": "string"
          },
          "detail": {
            "description": "Present on some refusals."
          },
          "orderId": {
            "type": "string",
            "description": "ON `quote_already_used` ONLY. The order that spent this quote: read it with `GET /api/v1/orders/{id}`. It is your own order: the quote resolved under your operator id, so this names nothing you could not already page out of `GET /api/v1/orders`.",
            "examples": [
              "ord_9f2c1b7a4e"
            ]
          }
        }
      },
      "OperatorEntryMoney": {
        "type": "object",
        "description": "THE ENTRY CONTRACT. Apply it; never recompute it. `authorizationAmount = actualDebit + releaseAmount` and `actualDebit = tradeAmount + platformFee + venueFee`, exactly.",
        "required": [
          "currency",
          "authorizationAmount",
          "actualDebit",
          "releaseAmount",
          "tradeAmount",
          "platformFee",
          "venueFee",
          "venueFeeKnown"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "examples": [
              "USD"
            ]
          },
          "authorizationAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Reserve this before the order. The gross cash the customer commits. The fee is already inside it."
          },
          "actualDebit": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Take this once the fill is known. Zero on a rejection or an unresolved order."
          },
          "releaseAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Give this back. Non-zero whenever less filled than was authorized."
          },
          "tradeAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "The part of the debit that bought contracts."
          },
          "platformFee": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Parity’s fee, taken off the top of the stake."
          },
          "venueFee": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Zero where no venue publishes one. Check `venueFeeKnown` before reporting it as a fact."
          },
          "venueFeeKnown": {
            "type": "boolean",
            "description": "False means `venueFee` is a placeholder zero, kept distinguishable from a known zero."
          }
        }
      },
      "OperatorExitMoney": {
        "type": "object",
        "description": "THE EXIT CONTRACT. A credit, never a debit. `sellProceeds` is ALREADY NET of both fees beside it. Subtracting them again is a double charge.",
        "required": [
          "currency",
          "sellProceeds",
          "platformFee",
          "venueFee",
          "venueFeeKnown"
        ],
        "properties": {
          "currency": {
            "type": "string"
          },
          "sellProceeds": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Credit this."
          },
          "platformFee": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Parity’s fee on the exit. A round trip is two transactions."
          },
          "venueFee": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "See `venueFeeKnown`."
          },
          "venueFeeKnown": {
            "type": "boolean"
          }
        }
      },
      "OperatorSettlementMoney": {
        "type": "object",
        "description": "Exactly one is non-zero; a loss zeroes both. Two names because they are different instructions to book.",
        "required": [
          "currency",
          "settlementCredit",
          "voidCredit"
        ],
        "properties": {
          "currency": {
            "type": "string"
          },
          "settlementCredit": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "A winning position’s payout."
          },
          "voidCredit": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "A cancelled market’s refund, fee included."
          }
        }
      },
      "OperatorPositionMoney": {
        "type": "object",
        "description": "Facts, NOT instructions. Neither field is money to move.",
        "required": [
          "currency",
          "costBasis",
          "realisedPnl"
        ],
        "properties": {
          "currency": {
            "type": "string"
          },
          "costBasis": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Cash already spent on what is still held."
          },
          "realisedPnl": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Booked by exits and settlements. Negative on a loss."
          }
        }
      },
      "Outcome": {
        "type": "object",
        "description": "One tradeable contract inside an event.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The opaque `ctr_…` contract id. This is what you quote against. Not the event id."
          },
          "label": {
            "type": "string"
          },
          "teamLogoUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The crest for this side of a team fixture, or `null`. Resolved from the venue’s own team list by an exact, league-scoped name match, so a contract whose team could not be matched with certainty carries `null` rather than an approximate badge. `null` is the common case: it covers every draw row, every non-team contract and every contract outside sport. Render nothing for `null`; do not substitute a placeholder crest. The URL points at the venue’s CDN and is not served by us, so treat it as it may rot and fall back to no image."
          },
          "teamColor": {
            "type": [
              "string",
              "null"
            ],
            "description": "That same team’s colour as `#rrggbb`, or `null`. Read off the SAME venue team record as `teamLogoUrl`, never looked up separately, so the badge and the colour can never describe two different clubs. `null` wherever the crest is `null`, and also on the 2.46% of crested rows the venue publishes no colour for, so a surface that tints by it needs a neutral fallback rather than a guarantee. Intended for a pill or a series colour; it is the club’s own colour and carries no meaning about price or direction."
          },
          "teamMarks": {
            "type": [
              "object",
              "null"
            ],
            "description": "BOTH clubs of a two-sided match result, keyed by canonical side, or `null`. `teamLogoUrl` and `teamColor` above describe ONE club, which is the right shape where the venue publishes a contract per side (a soccer three-way). A moneyline is a single contract whose two OUTCOMES are the two clubs, so those two fields are `null` on every one of them and this carries the pair instead. Keyed by `yes` and `no`, exactly as `outcomeLabels` is, so a club and the word printed for it cannot come apart. Either side may be `null` on its own where the venue resolved no team for it. `null` on everything that is not a two-sided fixture, which is most of the catalogue. Each side carries `logo`, `color`, `name`, `abbr` and `record`, all five read off one venue team record. `record` is that club’s season record as a string (`\"2-0-0\"` in the NFL, `\"95-60\"` in baseball, where the number of parts is a per-league convention and is not parsed), `null` where the venue publishes none, and absent entirely on rows last ingested before the field existed, so read it defensively.",
            "properties": {
              "yes": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "no": {
                "type": [
                  "object",
                  "null"
                ]
              }
            }
          },
          "teamAbbr": {
            "type": [
              "string",
              "null"
            ],
            "description": "That same team’s short code, lowercase as the venue publishes it (`dal`, `gb`, `nfo`), or `null`. Read off the SAME venue team record as `teamLogoUrl` and `teamColor`. `null` on the draw, on every non-team contract, and on any row ingested before the field existed, so a surface that shortens a name with it must fall back to the full label rather than to initials of its own."
          },
          "probability": {
            "type": [
              "number",
              "null"
            ],
            "description": "THE REFERENCE, 0, 1. Nobody trades at it."
          },
          "yesCents": {
            "type": [
              "number",
              "null"
            ],
            "description": "The reference in cents. Render as a probability."
          },
          "noCents": {
            "type": [
              "number",
              "null"
            ]
          },
          "executableYesCents": {
            "type": [
              "number",
              "null"
            ],
            "description": "THE PRICE a BUY would pay. The ask. `null` means we hold no usable book side for this contract: show “no price”, never the reference. ALWAYS `null` on the recurring five-minute crypto contracts. Those are priced from the live order book at quote time, and the catalogue does not carry a book fresh enough to publish as a price on a market that lives 300 seconds. Ask `POST /quotes` for one."
          },
          "executableNoCents": {
            "type": [
              "number",
              "null"
            ],
            "description": "Buying NO is selling YES, so it costs 1 − bid."
          },
          "spreadCents": {
            "type": [
              "number",
              "null"
            ],
            "description": "ask − bid. Wide means the reference is a poor guide to cost. `null` wherever the executable prices are null, including on every five-minute crypto contract."
          },
          "yesLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "The venue’s own word for the YES side: “Up” on a five-minute crypto contract, `null` on an ordinary Yes/No market. DISPLAY ONLY: the canonical sides stay YES and NO everywhere an order is placed. Fall back to your own “Yes” when it is null."
          },
          "noLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "The venue’s own word for the NO side. See `yesLabel`."
          },
          "sportsMarketType": {
            "type": [
              "string",
              "null"
            ],
            "description": "WHAT KIND OF BET THIS IS, in the venue’s own vocabulary: `moneyline`, `spreads`, `totals`, `team_totals`, `q3_spreads`, `first_half_totals`, `soccer_exact_score`, `exact_margin`. `null` on every contract that is not a sports market, and on any sports market from a venue that publishes no such taxonomy. Measured across a full walk of the open catalogue, 121,361 of 172,216 contracts (70.5%) carry one, across 156 distinct values. A RAW VENUE STRING, deliberately not normalised into a vocabulary of ours: it means exactly what the venue means by it, so you can map it onto your own sections without unpicking a translation first. It is the only field that says what a contract IS. A fixture arrives as several hundred contracts and nothing else on them distinguishes the spread from the total, which is a problem we solved for ourselves by parsing question text and do not recommend. Treat the value set as OPEN: the venue adds sports and each brings its own words, so file anything you do not recognise under a general heading rather than dropping it."
          },
          "priceBasis": {
            "type": [
              "string",
              "null"
            ],
            "description": "midpoint | last | source | none."
          },
          "priceUpdatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "priceFeed": {
            "type": [
              "string",
              "null"
            ]
          },
          "threshold": {
            "type": [
              "number",
              "null"
            ],
            "description": "The parsed strike, for ordering a ladder."
          },
          "direction": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "above",
              "below",
              null
            ],
            "description": "Which side of `threshold` this contract pays on: `above` for “will it reach $X”, `below` for “will it dip to $X”. Read at the source from the contract’s own question, which is often the only place it appears: whole families label a contract with a bare “72,500”. `threshold` alone is NOT enough to group or bracket contracts. `↑ 80,000` and `↓ 80,000` share a strike and buy contradictory propositions. `null` means unknown, and is not a default of `above`: fail closed on it."
          },
          "volume24h": {
            "type": [
              "number",
              "null"
            ],
            "description": "CONTRACTS TRADED AT THE VENUE in the last 24h: a **count of contracts/shares, not a dollar amount**, and the venue’s figure for all trading there rather than flow you or Parity routed. Measured against the cash actually exchanged the two run 1.8×, 9.3× apart, varying with the contract’s price, so no conversion exists and none is applied. Render it as a quantity, never with a currency symbol. `volume24hContracts` is the identical number under an unambiguous name."
          },
          "volume24hContracts": {
            "type": [
              "number",
              "null"
            ],
            "description": "Exactly `volume24h`, named for its unit. Prefer this field in new code."
          },
          "closesAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "description": "open | closed | resolved. The contract’s own state, not the event’s."
          },
          "tradable": {
            "type": "boolean",
            "description": "Exactly `executableYesCents !== null || executableNoCents !== null`. A statement about what THIS payload can price, not about whether the venue trades the contract: the five-minute crypto contracts report `false` here and are still quotable through `POST /quotes`, which prices them from the live book."
          },
          "tradingClosed": {
            "type": "boolean",
            "description": "TRADING HAS STOPPED on this contract, whatever `status` says. True once the close has passed, or once the venue itself stopped accepting orders. `status` is not this statement and cannot be read as one: it stays `open` until our settlement sweep notices, which can be hours. The same pair of gates `POST /quotes` applies, so a contract reporting `true` will be refused with `market_not_open`. Use it to label the state: `tradable: false` covers many temporary reasons and this one is permanent."
          },
          "priceSuspended": {
            "type": "boolean",
            "description": "OUR COPY OF THIS CONTRACT’S PRICE IS TOO OLD TO BET ON, at any size. Present only when true. The third reason the two executable fields can be null, and the one you could not previously tell from the others: `tradingClosed` separates \"this question is over\" from \"we have no price\", and this separates \"nobody is quoting it\" from \"we are quoting it and our number is behind\". `POST /quotes` refuses the same contracts with `price_stale`. It is recomputed from `priceUpdatedAt` on every read and clears itself the moment the feed catches up, so treat it as a pause and not as a verdict on the market."
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "One question, with its outcomes. Names no venue anywhere.",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": "string",
            "description": "The event PRIMARY shelf, for a single-label UI."
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every shelf this event sits on, and what `?category=` actually filters against. An event whose primary `category` is `economy` can legitimately be returned by `?category=politics` when `politics` is in this array. Always contains `category`."
          },
          "sport": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which sport, as the venue’s own slug (`soccer`, `nfl`, `cfb`), or `null` outside sport. One tier above `league`: a family slug such as `soccer` covers every competition under it, while a league-shaped slug such as `nfl` is its own sport and its own league. A SLUG, not a display name. The venue’s spelling of it is catalogue data and is not published on this object."
          },
          "league": {
            "type": [
              "string",
              "null"
            ],
            "description": "The competition this event belongs to, as the venue’s own league slug, or `null`. Resolved from venue league metadata rather than from the title. About 97% of sports events carry one; the rest are genuinely leagueless, such as a Ballon d’Or or a next-manager market. `null` outside sport."
          },
          "eventType": {
            "type": [
              "string",
              "null"
            ],
            "description": "`game` for a dated head-to-head fixture, `future` for a season-long question such as a champion or a win total, `null` outside sport. Use it to separate a shelf of this weekend’s fixtures from the outrights that would otherwise outrank them on volume."
          },
          "liveMatch": {
            "type": [
              "object",
              "null"
            ],
            "description": "THE STATE OF THE ACTUAL MATCH, on a sports fixture the venue is tracking, or `null`. `null` outside sport and `null` on a tracked sport the venue publishes nothing for, which is common: measured on 226 in-play fixtures, 106 carried this and 120 did not. It is the venue’s own reporting of the game, not Parity’s, and it is the only honest way to tell a fixture that is being played from one that finished hours ago. `closesAt` cannot: on a fixture it holds the KICKOFF time, so a passed close means the game started and says nothing about whether it ended. **Every field is published together or not at all, and `observedAt` is mandatory**. See its description for the rule that governs using any of this.",
            "properties": {
              "state": {
                "type": "string",
                "description": "`live` (being played), `ended` (finished), or `scheduled` (the venue knows the fixture and has not started reporting on it). Three values rather than two booleans, because a match cannot be both live and ended. **Do not render a score on `scheduled`**: the venue publishes a placeholder `0-0` on some fixtures that have not kicked off, and printing it states a result for a game nobody has played."
              },
              "score": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "THE SCORELINE AS THE VENUE WRITES IT, and deliberately unparsed: `24-32`, `7-6(7-4), 0-0` (tennis), `000-000|2-0|Bo3` (an esports series). Do not assume two integers separated by a hyphen. It is `null` on a fixture the venue flags without detail."
              },
              "period": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The venue’s own marker: `Q2`, `Bot 7th`, `1H`, `End Q2`, `VFT`."
              },
              "elapsed": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Clock within the period: `10:45`, `43`. Frequently absent even on a live fixture, so never depend on it."
              },
              "observedAt": {
                "type": "string",
                "format": "date-time",
                "description": "WHEN PARITY READ THESE FIELDS FROM THE VENUE. Mandatory whenever `liveMatch` is present, and this is the field that makes the rest safe to use: **a score is a fact about a moment and is worthless without its age**. This data rides the catalogue crawl, so an observation is routinely minutes old and goes permanently stale the instant our ingest stops. Check the age before you display any of it and show nothing when it is too old. Parity applies a 600s bound, derived from its own ingest cadence, and refuses the scoreline entirely past it. It records when we READ the value, not when the value last changed: a score that has not moved in ten minutes is a 0-0 at the seventieth minute, which is correct, so “unchanged” is not a staleness signal here."
              }
            },
            "required": [
              "state",
              "observedAt"
            ]
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "A Parity path (`/api/v1/images/{eventId}`), not the sourcing venue’s CDN."
          },
          "kind": {
            "type": "string",
            "description": "binary | mutually_exclusive | cumulative_ladder | ambiguous."
          },
          "outcomeCount": {
            "type": "integer"
          },
          "impliedLevel": {
            "type": [
              "number",
              "null"
            ]
          },
          "impliedLevelLabel": {
            "type": [
              "string",
              "null"
            ]
          },
          "closesAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "opensAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "volume": {
            "type": [
              "number",
              "null"
            ],
            "description": "CONTRACTS TRADED AT THE VENUE over the event’s lifetime, summed over its contracts: a **count, not a dollar amount**. See `Outcome.volume24h`. There is deliberately **no USD volume field**: neither venue publishes notional, and multiplying a lifetime count by today’s price would invent one."
          },
          "volume24h": {
            "type": [
              "number",
              "null"
            ],
            "description": "The same count over the last 24h. Contracts, not dollars."
          },
          "volumeContracts": {
            "type": [
              "number",
              "null"
            ],
            "description": "Exactly `volume`, named for its unit. Prefer this field in new code."
          },
          "volume24hContracts": {
            "type": [
              "number",
              "null"
            ],
            "description": "Exactly `volume24h`, named for its unit. Prefer this field in new code."
          },
          "outcomes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Outcome"
            }
          }
        }
      },
      "Category": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "eventCount": {
            "type": "integer"
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "marketId",
          "side"
        ],
        "description": "Send exactly one of `stake` or `contracts`. Accepting both and picking one silently would let a stale `stake` trade a size the caller did not intend, and the response would look entirely successful.",
        "properties": {
          "marketId": {
            "type": "string",
            "description": "The `ctr_…` CONTRACT id from the catalogue. An outcome, not an event."
          },
          "side": {
            "type": "string",
            "enum": [
              "YES",
              "NO"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "buy",
              "sell"
            ],
            "default": "buy"
          },
          "stake": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 1000000,
            "description": "Cash. Sizes a BUY."
          },
          "contracts": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000000,
            "description": "Quantity. Sizes a SELL, and is the only way to exit in full."
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "quoteId": {
            "type": "string"
          },
          "contractId": {
            "type": "string"
          },
          "side": {
            "type": "string",
            "enum": [
              "YES",
              "NO"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "buy",
              "sell"
            ]
          },
          "executionPrice": {
            "type": "number",
            "description": "What this quote executes at. SHOW THIS, not the probability."
          },
          "priceBasis": {
            "type": "string",
            "enum": [
              "book",
              "reference"
            ]
          },
          "priceSource": {
            "type": "string",
            "enum": [
              "venue_book",
              "stored_book",
              "stored_reference"
            ],
            "description": "WHERE the price came from, which `priceBasis` cannot say. `venue_book` was obtained from the venue for THIS quote; `stored_book` is our last ingested top-of-book; `stored_reference` is an estimate. A five-minute contract is always `venue_book`. The alternative is a refusal."
          },
          "depth": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "requestedContracts": {
                    "type": "number"
                  },
                  "availableContracts": {
                    "type": "number"
                  },
                  "levels": {
                    "type": "integer"
                  },
                  "partial": {
                    "type": "boolean"
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "What the book could absorb, where the price was walked against real levels. `partial: true` means the book was thinner than the trade and THIS QUOTE HAS BEEN SIZED DOWN to fit it. Contracts, stake and payout all describe the smaller trade. Null where no depth is published."
          },
          "priceQuality": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "midPrice": {
                    "type": "number"
                  },
                  "spread": {
                    "type": "number"
                  },
                  "impliedReturn": {
                    "type": "number"
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "WHAT THE SPREAD COSTS ON THIS TRADE. `executionPrice` is the ask, and your customer crosses the venue’s spread to reach it. `midPrice` is the midpoint of the two-sided book, the best available PROXY for fair value, which nobody has offered to trade at. `impliedReturn` is the value back per unit committed, valued at that midpoint and net of the Parity fee. The fee alone implies 0.988 at 120 bps; anything materially below that is the spread, and it accrues to the venue’s liquidity providers rather than to Parity. Worst at the bottom of the book, where the cost is proportional to `spread / 2 × midPrice`. Parity does not gate on it: a minimum price is a judgement about a customer, and the licence, the customer relationship and the affordability duty are yours. This is the measurement that judgement needs. Null where there is no honest number: a one-sided book, a crossed one, or a spread too wide for the midpoint to mean anything."
          },
          "stake": {
            "type": "number",
            "description": "Display. The exact figure is `operatorMoney.authorizationAmount`."
          },
          "platformFee": {
            "type": "number",
            "description": "Display. Parity’s fee on this trade."
          },
          "platformFeeBps": {
            "type": "number",
            "description": "The rate this quote was struck at, echoed so reconciliation asserts the terms agreed rather than what our config says today."
          },
          "predictaFee": {
            "type": "number",
            "deprecated": true,
            "description": "DEPRECATED ALIAS of `platformFee`, the name this API shipped under. Always present and always equal. Read `platformFee` in new code."
          },
          "predictaFeeBps": {
            "type": "number",
            "deprecated": true,
            "description": "DEPRECATED ALIAS of `platformFeeBps`. Always present and always equal."
          },
          "commercialTerms": {
            "type": "object",
            "properties": {
              "termsId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "feeBps": {
                "type": "number"
              },
              "revShareBps": {
                "type": "number"
              }
            }
          },
          "venueFee": {
            "type": [
              "number",
              "null"
            ]
          },
          "tradeAmount": {
            "type": "number"
          },
          "contracts": {
            "type": "number"
          },
          "potentialPayout": {
            "type": "number",
            "description": "Each contract settles at exactly $1.00 if the side is right."
          },
          "potentialProfit": {
            "type": "number"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Past this the order path answers `409 quote_expired`, and never silently re-prices."
          },
          "sourceTimestamp": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "simulated": {
            "type": "boolean",
            "description": "Always present, never omitted when inconvenient: a simulated fill that looks identical to a real one is what an integrator should not have to read the docs to discover."
          },
          "freshness": {
            "type": "object",
            "description": "The SERVER’s verdict on the price age. Render it rather than recomputing. The two disagreeing is the bug this field prevents.",
            "properties": {
              "level": {
                "type": "string"
              },
              "ageSeconds": {
                "type": "number"
              },
              "feed": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "routing": {
            "type": "object",
            "description": "How many venues could compete, and whether any may. The list is not published: naming the alternatives is naming the venues.",
            "properties": {
              "consideredVenues": {
                "type": "integer"
              },
              "routable": {
                "type": "boolean"
              }
            }
          },
          "operatorMoney": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorEntryMoney"
              },
              {
                "type": "null"
              }
            ],
            "description": "Populated on a BUY."
          },
          "operatorExitMoney": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorExitMoney"
              },
              {
                "type": "null"
              }
            ],
            "description": "Populated on a SELL."
          }
        }
      },
      "OrderRequest": {
        "type": "object",
        "required": [
          "quoteId",
          "clientOrderId"
        ],
        "properties": {
          "quoteId": {
            "type": "string",
            "description": "Spendable once."
          },
          "clientOrderId": {
            "type": "string",
            "maxLength": 200,
            "description": "YOUR id for this order, and the idempotency key. Reuse it on every retry of the same intent."
          },
          "executionIntent": {
            "type": "string",
            "enum": [
              "record_intent",
              "simulate"
            ],
            "description": "RECORD THE ORDER, OR SIMULATE A FILL. Optional; the default depends on your tenant.\n\n`record_intent` records a real order durably and reserves against it. The response\ncarries no fill and an `actualDebit` of `0.00`, because nothing is spent at the\nmoment an order is recorded. It is the DEFAULT for a live tenant, and the order\nbecomes executable when you confirm the reserve.\n\n`simulate` fills the order against the simulated venue and produces a position and a\ndebit. It is the DEFAULT for a sandbox tenant, because a sandbox exists so that a\nfill can be made to happen. A tenant whose execution mode is above `simulated` is\nrefused it with `422 simulation_not_permitted`."
          },
          "maxSlippage": {
            "type": "number",
            "minimum": 0,
            "exclusiveMaximum": 1,
            "description": "HOW MUCH WORSE THAN THE QUOTED PRICE YOU WILL STILL ACCEPT, in price units.\nOptional. Omitted is `0`, and `0` is the guarantee: the price on the quote is the\nprice your customer gets, or the order does not fill.\n\nONE SIDED. It widens the adverse direction only: above the quoted price on a buy,\nbelow it on a sell. Price improvement is always passed straight through and is never\nclawed back, so there is nothing to allow for in that direction.\n\nSend it when you would rather own the contract than re-quote: on a five-minute\nmarket the book can move a tick between the quote and the match. It is CLAMPED to\nthe platform maximum (0.05) rather than refused, it can never take a buy past the\n95c wagerable ceiling, and the allowance actually granted is what the order is\nfilled and judged against."
          },
          "reserveReference": {
            "type": "string",
            "maxLength": 200,
            "description": "YOUR id for a hold you have ALREADY placed on the customer’s funds. Present, the reserve is confirmed in this call and the order comes back `pending_venue_execution`. Absent, the order is `reserve_pending` and CANNOT execute until `POST /api/v1/orders/{id}/confirm-reserve`: silence is not a confirmation that somebody’s money is held."
          }
        }
      },
      "Order": {
        "type": "object",
        "properties": {
          "orderId": {
            "type": "string"
          },
          "clientOrderId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "reserve_pending",
              "pending_venue_execution",
              "submitted",
              "partially_filled",
              "filled",
              "cancelled",
              "rejected"
            ],
            "description": "`submitted` is the UNRESOLVED state: the venue was asked and has not answered. It is\nnot a failure.\n\n`reserve_pending` and `pending_venue_execution` are the two states of an order that\nhas been RECORDED and transmitted nowhere. They differ in one thing: whether you have\nconfirmed the customer’s funds are held. Only the second can ever execute."
          },
          "contractId": {
            "type": "string"
          },
          "side": {
            "type": "string",
            "enum": [
              "YES",
              "NO"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "buy",
              "sell"
            ],
            "description": "Decides which money object is populated."
          },
          "filledQuantity": {
            "type": "number",
            "description": "What the customer actually holds. May be less than the quote’s `contracts`."
          },
          "averagePrice": {
            "type": [
              "number",
              "null"
            ]
          },
          "platformFee": {
            "type": "number",
            "description": "Display. Parity’s fee on this trade."
          },
          "platformFeeBps": {
            "type": "number"
          },
          "predictaFee": {
            "type": "number",
            "deprecated": true,
            "description": "DEPRECATED ALIAS of `platformFee`, the name this API shipped under. Always present and always equal."
          },
          "predictaFeeBps": {
            "type": "number",
            "deprecated": true,
            "description": "DEPRECATED ALIAS of `platformFeeBps`. Always present and always equal."
          },
          "notional": {
            "type": "number"
          },
          "stake": {
            "type": "number"
          },
          "rejectReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "simulated": {
            "type": "boolean",
            "description": "Whether THIS order was filled by the simulation rather than by a venue. Per order, derived from the venue that answered it, and no longer a literal: it used to be written `true` on every insert and never cleared, so a real fill would have reported `simulated: true`. One deployment carries sandbox and live tenants at the same time, so read this field on the order in front of you and do not infer it from the host name, the key prefix or another order."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "routing": {
            "type": "object",
            "properties": {
              "reason": {
                "type": "string"
              },
              "consideredVenues": {
                "type": "integer"
              }
            }
          },
          "idempotentReplay": {
            "type": "boolean",
            "description": "True when this response replayed an order that already existed."
          },
          "operatorMoney": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorEntryMoney"
              },
              {
                "type": "null"
              }
            ],
            "description": "Non-null on a BUY. NULL ON A SELL, deliberately: an exit carrying an `authorizationAmount` would have a wallet debit a customer for selling their own position."
          },
          "operatorExitMoney": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorExitMoney"
              },
              {
                "type": "null"
              }
            ],
            "description": "Non-null on a SELL."
          },
          "settlementState": {
            "type": "string",
            "enum": [
              "settled",
              "pending"
            ],
            "description": "THE FIELD A RECOVERING INTEGRATION READS FIRST, and it is on EVERY order this API returns: the create response, both single-order reads, the cancel response and the paged list.  `pending` means HOLD the authorization and poll. Do not release. An order awaiting execution is pending for the same reason an unresolved one is: it may still execute, and so may a part fill the venue can still add to. To tell those two apart, read `transmissionState`.  `settled` means the money is final. Apply `operatorMoney` and move on."
          },
          "transmissionState": {
            "type": "string",
            "enum": [
              "not_transmitted",
              "unknown",
              "resolved"
            ],
            "description": "HAS ANYTHING BEEN SENT TO A VENUE? It does not replace `settlementState`: that field\nanswers “may I apply the money yet”, this one answers “may I take it back”.\n\n`not_transmitted`: provably nothing was sent. A cancellation here is safe and returns\na release instruction.\n`unknown`: it may or may not be at a venue, OR it is a part fill the venue can\nstill add to. NEVER release, NEVER cancel. Hold the authorization and poll.\n`resolved`: the venue is FINISHED with this order: it filled, refused, or closed\nwhat was left. Apply `operatorMoney`.\n\nA PART FILL IS `unknown`, NOT `resolved`, WHILE THE VENUE IS STILL WORKING IT.\n`partially_filled` means some quantity is done and the remainder is still committed.\nReading that as `resolved` would have an operator release the untraded remainder,\nand the next fill would then spend collateral against money the customer already\nhas back: irrecoverable, because the position is real and the cash is gone. The\nstate flips to `resolved` when the venue closes the order, and only then."
          },
          "intent": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/OrderIntent"
              },
              {
                "type": "null"
              }
            ],
            "description": "Present on an order that was RECORDED rather than executed, and on the two single-order reads. Null on the paged list."
          }
        }
      },
      "OrderIntent": {
        "type": "object",
        "description": "The reserve confirmation and, once withdrawn, the release instruction. The two facts that pass between Parity and an operator around an order nothing has been sent for.",
        "properties": {
          "reserveState": {
            "type": "string",
            "enum": [
              "awaiting_reserve",
              "reserved",
              "released"
            ],
            "description": "`awaiting_reserve` until you confirm the hold; `reserved` afterwards; `released` once the order is cancelled."
          },
          "reserveConfirmedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reserveReference": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for the hold. Stored, never interpreted. The FIRST confirmation’s reference is the one kept."
          },
          "executable": {
            "type": "boolean",
            "description": "Whether this order is cleared to execute. FALSE for an unconfirmed reserve: Parity cannot execute before your reserve succeeded, and this is that guarantee as a field you can assert on."
          },
          "cancellation": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ReleaseInstruction"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ReleaseInstruction": {
        "type": "object",
        "description": "WHAT TO HAND BACK, issued exactly once. `authorizationAmount = actualDebit + releaseAmount` closes here exactly as it does on a fill, and `actualDebit` is always `0.00` because a cancelled order spent nothing.",
        "required": [
          "cancellationId",
          "currency",
          "authorizationAmount",
          "actualDebit",
          "releaseAmount",
          "reason",
          "issuedAt",
          "idempotentReplay"
        ],
        "properties": {
          "cancellationId": {
            "type": "string",
            "description": "Stable across every replay. Two ids for one cancellation is how a customer gets credited twice."
          },
          "currency": {
            "type": "string",
            "examples": [
              "USD"
            ]
          },
          "authorizationAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "What you were asked to reserve."
          },
          "actualDebit": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Always `0.00`: nothing was transmitted, so nothing was spent."
          },
          "releaseAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Hand this back."
          },
          "reason": {
            "type": "string"
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time"
          },
          "idempotentReplay": {
            "type": "boolean",
            "description": "True when this replays an instruction already issued for this order."
          }
        }
      },
      "ConfirmReserveRequest": {
        "type": "object",
        "properties": {
          "reserveReference": {
            "type": "string",
            "maxLength": 200,
            "description": "YOUR id for the hold. Optional, stored, never interpreted. Your half of the audit trail. If a retry carries a different reference the FIRST one is kept: the hold that actually authorised this order is the one that got here first."
          }
        }
      },
      "CancelOrderRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 300,
            "description": "Your own words for why. Stored, and echoed on every replay of the instruction."
          }
        }
      },
      "OrderCancellation": {
        "allOf": [
          {
            "$ref": "#/components/schemas/OrderDetail"
          },
          {
            "type": "object",
            "properties": {
              "cancellation": {
                "$ref": "#/components/schemas/ReleaseInstruction"
              }
            }
          }
        ]
      },
      "OrderDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Order"
          },
          {
            "type": "object",
            "properties": {
              "externalUserId": {
                "type": "string"
              },
              "fills": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Fill"
                },
                "description": "Only when `?fills=true`."
              }
            }
          }
        ]
      },
      "Fill": {
        "type": "object",
        "properties": {
          "fillId": {
            "type": "string"
          },
          "orderId": {
            "type": "string"
          },
          "quantity": {
            "type": "number"
          },
          "price": {
            "type": "number"
          },
          "filledAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Position": {
        "type": "object",
        "properties": {
          "positionId": {
            "type": "string"
          },
          "externalUserId": {
            "type": "string"
          },
          "contractId": {
            "type": "string",
            "description": "Derived from the LISTING the position is actually keyed on: a position settles by that contract’s rules."
          },
          "side": {
            "type": "string",
            "enum": [
              "YES",
              "NO"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "closed",
              "settled"
            ]
          },
          "quantity": {
            "type": "number",
            "description": "Contracts held. Size a SELL with this."
          },
          "averagePrice": {
            "type": "number",
            "description": "Weighted average entry price. A second buy folds in rather than opening a second row."
          },
          "openedAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "operatorMoney": {
            "$ref": "#/components/schemas/OperatorPositionMoney"
          }
        }
      },
      "Settlement": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Parity’s stable id for this settlement. Safe as the idempotency key for a credit."
          },
          "positionId": {
            "type": "string"
          },
          "contractId": {
            "type": "string",
            "examples": [
              "ctr_9f2a1c7b4e6d8035"
            ],
            "description": "The opaque contract id, the same one `/events`, `/quotes`, `/orders` and `/positions` publish. Emitted since this feed shipped and previously missing from this document."
          },
          "externalUserId": {
            "type": "string"
          },
          "side": {
            "type": "string"
          },
          "resolution": {
            "type": "string"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "won",
              "lost",
              "void"
            ]
          },
          "contracts": {
            "type": "number"
          },
          "realisedPnl": {
            "type": "number",
            "description": "THIS settlement’s figure, not the position’s lifetime total."
          },
          "lifetimeRealisedPnl": {
            "type": "number"
          },
          "settledAt": {
            "type": "string",
            "format": "date-time"
          },
          "operatorMoney": {
            "$ref": "#/components/schemas/OperatorSettlementMoney"
          },
          "cursor": {
            "type": "string"
          }
        }
      },
      "PositionSettlement": {
        "type": "object",
        "description": "One settled position on the deprecated feed. The record is NARROWED at the boundary: Parity’s internal `operatorUserId`, the `venue` and the `venueListingId` are not published. `venueListingId` was `${provider}:${providerMarketId}`, which named the source and handed over its own identifier for the contract; `contractId` replaces it and is the same opaque id every other endpoint uses. Correlate on `settlementId`, `positionId` and `contractId`.",
        "required": [
          "settlementId",
          "positionId",
          "contractId",
          "side",
          "resolution",
          "outcome",
          "contracts",
          "payout",
          "realisedPnl",
          "lifetimeRealisedPnl",
          "settledAt",
          "operatorMoney"
        ],
        "properties": {
          "settlementId": {
            "type": "string",
            "description": "Safe as the idempotency key for a credit."
          },
          "positionId": {
            "type": "string",
            "description": "Stable and unique. Re-applying one record is a no-op."
          },
          "contractId": {
            "type": "string",
            "examples": [
              "ctr_9f2a1c7b4e6d8035"
            ]
          },
          "side": {
            "type": "string"
          },
          "resolution": {
            "type": "string",
            "description": "The source’s outcome: `YES`, `NO` or `VOID`."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "won",
              "lost",
              "void"
            ]
          },
          "contracts": {
            "type": "number"
          },
          "payout": {
            "type": "number",
            "description": "Display. Book from `operatorMoney`."
          },
          "realisedPnl": {
            "type": "number",
            "description": "THIS settlement’s figure."
          },
          "lifetimeRealisedPnl": {
            "type": "number",
            "description": "The position’s whole life, exits included."
          },
          "settledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Also the cursor: pass it back as `?since=`."
          },
          "operatorMoney": {
            "$ref": "#/components/schemas/OperatorSettlementMoney"
          }
        }
      },
      "FeedItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The underlying fact’s id. Safe as an idempotency key."
          },
          "type": {
            "type": "string",
            "enum": [
              "order.state_changed",
              "order.filled",
              "fee.assessed",
              "collateral.moved",
              "ledger.posted",
              "position.exited",
              "position.settled",
              "webhook.emitted"
            ]
          },
          "cursor": {
            "type": "string"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "externalUserId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null for operator-level facts. Never Parity’s internal uuid."
          },
          "positionId": {
            "type": [
              "string",
              "null"
            ]
          },
          "orderId": {
            "type": [
              "string",
              "null"
            ]
          },
          "money": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Exact decimal strings, or null where the fact carries no money."
          },
          "detail": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "ReconciliationReport": {
        "type": "object",
        "properties": {
          "ranAt": {
            "type": "string",
            "format": "date-time"
          },
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "ok": {
            "type": "boolean"
          },
          "trialBalance": {
            "type": "object",
            "additionalProperties": true,
            "description": "Across every line: debits equal credits. A difference means something wrote outside the posting engine."
          },
          "cashStatement": {
            "type": "object",
            "additionalProperties": true,
            "description": "Carries both `expectedEnding` and `ending`, because a statement that only reports the number it computed cannot fail."
          },
          "collateralStatement": {
            "type": "object",
            "additionalProperties": true,
            "description": "THE SAME IDENTITY OVER YOUR COLLATERAL, which is where your money is: every cash leg of every trade posts to your venue float, not to a Parity-held player balance. `cashStatement` covers the hosted pot and is all zeroes unless Parity holds your players’ wallets. Read `accountsInScope` on both: `reconciled: true` over zero accounts means there was nothing to reconcile, not that the money ties."
          },
          "scanned": {
            "type": "object",
            "properties": {
              "entries": {
                "type": "integer"
              },
              "lines": {
                "type": "integer"
              },
              "accounts": {
                "type": "integer"
              }
            }
          },
          "venueAgreement": {
            "type": "object",
            "properties": {
              "exits": {
                "type": "integer"
              },
              "agrees": {
                "type": "integer"
              },
              "disagrees": {
                "type": "integer"
              },
              "unknown": {
                "type": "integer"
              }
            }
          },
          "findings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReconciliationFinding"
            }
          }
        }
      },
      "ReconciliationFinding": {
        "type": "object",
        "properties": {
          "check": {
            "type": "string"
          },
          "severity": {
            "type": "string",
            "enum": [
              "critical",
              "warning"
            ]
          },
          "detail": {
            "type": "string"
          },
          "subjectType": {
            "type": "string",
            "enum": [
              "user",
              "record"
            ]
          },
          "subjectId": {
            "type": "string",
            "description": "YOUR id for a customer when `subjectType` is `user`; a Parity id you can quote back to us when it is `record`."
          }
        }
      },
      "SandboxBalance": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "sandbox",
              "live"
            ],
            "description": "From the operator record, not from anything in the request."
          },
          "available": {
            "type": "number"
          },
          "reserved": {
            "type": "number"
          },
          "cash": {
            "type": "number",
            "description": "`available + reserved`. The only figure here that is cash."
          },
          "positionCost": {
            "type": "number",
            "description": "What open contracts COST. Not cash, not a mark, deliberately outside the total."
          },
          "asOf": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Movement": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "SANDBOX_CREDIT | DEPOSIT | WITHDRAWAL | ORDER_RESERVE | RESERVE_RELEASE | BUY |\nFEE | SELL | SETTLEMENT | VOID_REFUND | ADJUSTMENT | HOSTED_SPEND | HOSTED_PAYOUT |\nVENUE_FEE.\n\nDELIBERATELY NOT AN ENUM. The feed publishes every entry Parity posts, so a new\nmovement reaches this field without a new API version, and a client that validates\nagainst a closed list would start rejecting statements it had been reading fine.\nHandle what you recognise and pass the rest through.\n\nThe last three are the hosted-wallet pair and the venue’s own fee.\n`HOSTED_SPEND` reduces a Parity-held balance when a fill draws the operator float;\n`HOSTED_PAYOUT` raises it on a sale, a win or a void refund; `VENUE_FEE` is the fee\nthe venue itself kept on a fill. The two hosted movements appear only for a customer\nwhose wallet Parity holds, and they net to zero on the operator float over a whole\ntrade."
          },
          "amount": {
            "type": "number",
            "description": "Always positive. The direction lives in `type`."
          },
          "currency": {
            "type": "string"
          },
          "referenceType": {
            "type": [
              "string",
              "null"
            ]
          },
          "referenceId": {
            "type": [
              "string",
              "null"
            ]
          },
          "memo": {
            "type": [
              "string",
              "null"
            ]
          },
          "correctsEntryId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Non-null on a correction, naming the entry it supersedes. Neither is ever removed."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SupportedAssets": {
        "type": "object",
        "properties": {
          "retrievedAt": {
            "type": "string",
            "format": "date-time",
            "description": "So a caller can see the age of what they hold rather than assume it is current."
          },
          "count": {
            "type": "integer"
          },
          "assets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SupportedAsset"
            }
          },
          "symbols": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "depositsEnabled": {
            "type": "boolean",
            "description": "False when no treasury wallet is configured; the catalogue is still real and readable."
          },
          "depositsDisabledBecause": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SupportedAsset": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "chainId": {
            "type": "string"
          },
          "network": {
            "type": "string"
          },
          "tokenAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "decimals": {
            "type": "integer",
            "description": "Required to render an amount: base units are integers, a display amount is not."
          },
          "minimumUsd": {
            "type": "number",
            "description": "The ROUTE’s own floor, not ours. Below it a transfer may not bridge at all, and the funds are stranded rather than refunded."
          },
          "addressKind": {
            "type": "string",
            "enum": [
              "evm",
              "svm",
              "btc",
              "tron"
            ]
          }
        }
      },
      "DepositAddress": {
        "type": "object",
        "properties": {
          "depositId": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "asset": {
            "type": "string"
          },
          "chainId": {
            "type": "string"
          },
          "network": {
            "type": "string"
          },
          "depositAddress": {
            "type": "string"
          },
          "tokenAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "decimals": {
            "type": "integer"
          },
          "minimumUsd": {
            "type": "number"
          },
          "addressKind": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "pollUrl": {
            "type": "string",
            "description": "Follow this rather than building the path yourself."
          }
        }
      },
      "Deposit": {
        "type": "object",
        "properties": {
          "depositId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "awaiting_funds",
              "detected",
              "bridging",
              "credited",
              "failed"
            ],
            "description": "Ours. Only `credited` means the customer has the money."
          },
          "providerStatus": {
            "type": [
              "string",
              "null"
            ],
            "description": "The bridge’s own word, unmapped, so a support thread can quote it verbatim. Not a customer-facing string."
          },
          "eligibleToCredit": {
            "type": "boolean",
            "description": "The chain is finished and nothing internal has moved. NOT a credit."
          },
          "asset": {
            "type": "string"
          },
          "chainId": {
            "type": "string"
          },
          "network": {
            "type": "string"
          },
          "depositAddress": {
            "type": "string"
          },
          "minimumUsd": {
            "type": [
              "number",
              "null"
            ]
          },
          "sourceTxHash": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceAmountBaseUnit": {
            "type": [
              "string",
              "null"
            ]
          },
          "creditedAmount": {
            "type": [
              "number",
              "null"
            ],
            "description": "Non-null only once a ledger entry exists."
          },
          "ledgerEntryId": {
            "type": [
              "string",
              "null"
            ]
          },
          "failureReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "timeline": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "from": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "to": {
                  "type": "string"
                },
                "at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "syncError": {
            "type": "string",
            "description": "Present when the bridge could not be polled. The stored lifecycle is still authoritative for everything already recorded."
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "description": "ALL SIX, AND EVERY ONE OF THEM IS EMITTED. The subscription list used to offer five, omitting `order.intent_recorded` and `order.cancelled`. Those are the two an operator running live most needs, because under `record_intent` no venue is contacted and those are the only two things that ever happen to an order. An endpoint registered with an empty `events` array has always received every type; what was missing was the ability to ask for these two by name. `position.updated` has been REMOVED: it was subscribable and documented and no code path ever raised one, so a handler written for it would have waited forever. Every change a position can undergo is reported by `order.filled` (an entry or an exit fill) or by `position.settled` (the resolution), and both carry the money block to book from. A registration that names `position.updated` is now refused rather than silently accepted.",
        "enum": [
          "order.intent_recorded",
          "order.filled",
          "order.rejected",
          "order.cancelled",
          "position.settled",
          "market.resolution_changed"
        ]
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "Every delivery has this shape. `data` is the event’s own payload; the schemas below say what is in it per type.",
        "required": [
          "id",
          "type",
          "dedupeKey",
          "createdAt",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The EVENT’s id. Stable across every retry of it."
          },
          "type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "dedupeKey": {
            "type": "string",
            "description": "The natural key of the FACT, not of this attempt to report it. Key on it: at-least-once is the only guarantee an HTTP retry can offer."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Stamped when THIS ATTEMPT is sent. It moves between retries, so it is a delivery timestamp and must not be used to order events or to date the fact."
          }
        }
      },
      "WebhookEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "additionalProperties": true,
                "description": "The fact. Narrowed to the partner form before it is stored, so an event carries the opaque `contractId` and never a source name, a source-side contract id, or Parity’s internal user uuid."
              }
            }
          }
        ]
      },
      "WebhookEventOrderFilled": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/OrderFilledEvent"
              }
            }
          }
        ]
      },
      "OrderFilledEvent": {
        "type": "object",
        "description": "ONE FILL. A partial fill is the case where the debit is NOT the authorization, which is the case this payload exists for.",
        "required": [
          "orderId",
          "clientOrderId",
          "externalUserId",
          "fillId",
          "contractId",
          "side",
          "action",
          "fillQuantity",
          "fillPrice",
          "filledQuantity",
          "status",
          "transmissionState",
          "operatorMoney",
          "operatorExitMoney",
          "simulated"
        ],
        "properties": {
          "orderId": {
            "type": "string"
          },
          "clientOrderId": {
            "type": "string",
            "description": "Your own id for the order."
          },
          "externalUserId": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for the customer. Null only if the identity row could not be read; a webhook is never allowed to fail a trade over one."
          },
          "fillId": {
            "type": "string",
            "description": "Parity’s id for THIS fill, and the tail of `dedupeKey`. One order can raise several of these."
          },
          "contractId": {
            "type": "string",
            "description": "Opaque `ctr_…`, the same id every other endpoint publishes."
          },
          "side": {
            "type": "string",
            "examples": [
              "YES"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "buy",
              "sell"
            ]
          },
          "fillQuantity": {
            "type": "number",
            "description": "Contracts moved by THIS fill."
          },
          "fillPrice": {
            "type": "number",
            "description": "The price THIS fill crossed at."
          },
          "filledQuantity": {
            "type": "number",
            "description": "The ORDER’s running total across every fill so far."
          },
          "averagePrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "The ORDER’s average across every fill so far."
          },
          "status": {
            "type": "string",
            "enum": [
              "filled",
              "partially_filled"
            ]
          },
          "transmissionState": {
            "type": "string",
            "enum": [
              "not_transmitted",
              "unknown",
              "resolved"
            ],
            "description": "READ THIS BEFORE ACTING ON `releaseAmount`. `unknown` means the venue can still add to this order, so the unspent remainder of the authorization is NOT yours to hand back yet. `resolved` means no further fill can arrive."
          },
          "operatorMoney": {
            "description": "CUMULATIVE FOR THE ORDER, never per fill. Null on a sell.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorEntryMoney"
              },
              {
                "type": "null"
              }
            ]
          },
          "operatorExitMoney": {
            "description": "CUMULATIVE FOR THE ORDER. Null on a buy.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorExitMoney"
              },
              {
                "type": "null"
              }
            ]
          },
          "simulated": {
            "type": "boolean",
            "description": "Whether the venue that filled this was the simulation. Derived from the venue, not a literal."
          }
        }
      },
      "WebhookEventOrderRejected": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/OrderRejectedEvent"
              }
            }
          }
        ]
      },
      "OrderRejectedEvent": {
        "type": "object",
        "required": [
          "orderId",
          "clientOrderId",
          "externalUserId",
          "reason",
          "operatorMoney"
        ],
        "properties": {
          "orderId": {
            "type": "string"
          },
          "clientOrderId": {
            "type": "string"
          },
          "externalUserId": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for the customer."
          },
          "reason": {
            "type": "string"
          },
          "operatorMoney": {
            "description": "Nothing was spent, so `actualDebit` is `0.00` and `releaseAmount` is the whole authorization. Null on a sell: an exit reserves no cash.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/OperatorEntryMoney"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "WebhookEventOrderIntentRecorded": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/OrderIntentRecordedEvent"
              }
            }
          }
        ]
      },
      "OrderIntentRecordedEvent": {
        "type": "object",
        "description": "A real order, funded, transmitted nowhere. HOLD the amount; do not release it.",
        "required": [
          "orderId",
          "clientOrderId",
          "externalUserId",
          "authorizationAmount",
          "currency",
          "transmitted"
        ],
        "properties": {
          "orderId": {
            "type": "string"
          },
          "clientOrderId": {
            "type": "string"
          },
          "externalUserId": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for the customer."
          },
          "authorizationAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Reserve this and HOLD it. There is no `releaseAmount` on this event, deliberately: nothing has been decided."
          },
          "currency": {
            "type": "string",
            "examples": [
              "USD"
            ]
          },
          "transmitted": {
            "type": "boolean",
            "description": "Always false. That is the whole point of the event."
          }
        }
      },
      "WebhookEventOrderCancelled": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/OrderCancelledEvent"
              }
            }
          }
        ]
      },
      "OrderCancelledEvent": {
        "type": "object",
        "description": "The release instruction. `authorizationAmount = actualDebit + releaseAmount`, exactly.",
        "required": [
          "orderId",
          "externalUserId",
          "cancellationId",
          "currency",
          "authorizationAmount",
          "actualDebit",
          "releaseAmount",
          "reason",
          "transmitted"
        ],
        "properties": {
          "orderId": {
            "type": "string"
          },
          "externalUserId": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for the customer."
          },
          "cancellationId": {
            "type": "string",
            "description": "The idempotency key for the release."
          },
          "currency": {
            "type": "string",
            "examples": [
              "USD"
            ]
          },
          "authorizationAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "What was reserved."
          },
          "actualDebit": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Always `0.00`: nothing was transmitted, so nothing was spent."
          },
          "releaseAmount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "examples": [
              "25.00"
            ],
            "description": "Hand this back."
          },
          "reason": {
            "type": "string"
          },
          "transmitted": {
            "type": "boolean",
            "description": "Always false."
          }
        }
      },
      "WebhookEventPositionSettled": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/PositionSettledEvent"
              }
            }
          }
        ]
      },
      "PositionSettledEvent": {
        "type": "object",
        "description": "CREDIT `operatorMoney`, not `payout`. The float is the figure the settlement row records; the block is the instruction, and it carries a currency and tells a refund apart from winnings.",
        "required": [
          "positionId",
          "settlementId",
          "externalUserId",
          "contractId",
          "side",
          "resolution",
          "outcome",
          "contracts",
          "costBasis",
          "entryFees",
          "payout",
          "realisedPnl",
          "lifetimeRealisedPnl",
          "operatorMoney",
          "simulated"
        ],
        "properties": {
          "positionId": {
            "type": "string"
          },
          "settlementId": {
            "type": "string",
            "description": "The idempotency key for the credit."
          },
          "externalUserId": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for the customer. Parity’s internal uuid is never published."
          },
          "contractId": {
            "type": "string",
            "description": "Opaque `ctr_…`."
          },
          "side": {
            "type": "string"
          },
          "resolution": {
            "type": "string",
            "enum": [
              "YES",
              "NO",
              "VOID"
            ]
          },
          "outcome": {
            "type": "string",
            "enum": [
              "won",
              "lost",
              "void"
            ]
          },
          "contracts": {
            "type": "number"
          },
          "costBasis": {
            "type": "number",
            "description": "A fact, not an instruction."
          },
          "entryFees": {
            "type": "number",
            "description": "A fact, not an instruction."
          },
          "payout": {
            "type": "number",
            "description": "The settlement row’s own figure. `operatorMoney` is what a wallet acts on."
          },
          "realisedPnl": {
            "type": "number",
            "description": "THIS settlement’s P&L."
          },
          "lifetimeRealisedPnl": {
            "type": "number",
            "description": "Everything the position ever realised, exits included."
          },
          "operatorMoney": {
            "$ref": "#/components/schemas/OperatorSettlementMoney"
          },
          "simulated": {
            "type": "boolean",
            "description": "Derived from the venue the position was built at, not a literal."
          }
        }
      }
    }
  }
}
