{
  "openapi": "3.1.0",
  "info": {
    "title": "Predicta Partner API",
    "version": "1.0.0",
    "summary": "Embedded prediction markets. The operator keeps the customer, the wallet and the brand.",
    "description": "## The architecture, before anything else\n\n**The OPERATOR owns its customers’ cash ledger. Predicta never holds player funds.**\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 Predicta’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`, `predictaFee`, `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-Predicta-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 Predicta-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,961 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` the book publishes no usable side: show “no price” and never fall back to the\nreference.\n\n## Order lifecycle, and the state that is not a failure\n\n```\naccepted ─▶ submitted ─▶ filled | partially_filled | rejected\n                │\n                └─▶ unknown after timeout ─▶ reconciliation_required\n```\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 Predicta 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## 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.\nLoop on `nextCursor`; **a last page legitimately has no `nextCursor`**, and a short page is\nnot itself a promise that there is nothing more.",
    "contact": {
      "name": "Predicta",
      "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.predicta.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": "Sandbox",
      "description": "Endpoints that model a Predicta-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 Predicta’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": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "trending",
                "volume",
                "closing-soon",
                "new"
              ]
            }
          },
          {
            "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 contracts with a usable executable side."
          },
          {
            "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"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer",
                          "description": "Size of the whole filtered set, not what remains after the cursor."
                        },
                        "nextCursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "hasMore": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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 Predicta-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.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "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 Predicta-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",
                  "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 Predicta-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 Predicta’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/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-Predicta-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The OPERATOR'S own id for the end user. Predicta 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-Predicta-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": "This operator’s offering policy does not include this market — the id from our own catalogue was never wrong, so this is not a 404.\n\nCodes: `market_not_offered`, `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 price behind the quote is older than the router will trade on. Re-quote.\n\nCodes: `price_stale`.",
            "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-Predicta-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The OPERATOR'S own id for the end user. Predicta 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-Predicta-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 policy has since disowned the market.\n\nCodes: `quote_not_yours`, `market_not_offered`, `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. `reconciliation_required` is an UNKNOWN, not a failure: collateral stays committed.\n\nCodes: `quote_expired`, `quote_already_used`, `price_stale`, `reconciliation_required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The order cannot be placed as asked.\n\nCodes: `market_unavailable`, `no_eligible_venue`, `insufficient_position`, `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. Predicta 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",
                  "properties": {
                    "orders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrderDetail"
                      }
                    },
                    "count": {
                      "type": "integer"
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`clientOrderId` without `externalUserId`.\n\nCodes: `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 Predicta-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": "Predicta’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 Predicta-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/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. Predicta 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",
                  "properties": {
                    "positions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Position"
                      }
                    },
                    "count": {
                      "type": "integer"
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "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 Predicta-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 Predicta-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. Predicta 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",
                  "properties": {
                    "settlements": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Settlement"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "hasMore": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "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 Predicta-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.",
        "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",
                  "properties": {
                    "settlements": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Pass back as `?since=`."
                    },
                    "count": {
                      "type": "integer"
                    },
                    "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 Predicta-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 Predicta, 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 Predicta 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 Predicta-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. Predicta 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",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedItem"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "hasMore": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "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 Predicta-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 Predicta-held balance — SANDBOX ONLY",
        "description": "**`403 sandbox_only` in live mode.** This models a per-user balance held by Predicta,\nwhich a live operator does not have: in production the OPERATOR owns its customers’ cash\nledger and Predicta 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-Predicta-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 Predicta-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\nThe debit/credit LINES are not exposed: publishing them would make Predicta’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"
                      ]
                    }
                  }
                }
              }
            }
          },
          "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 Predicta-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/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 Predicta-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-Predicta-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The OPERATOR'S own id for the end user. Predicta 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-Predicta-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 Predicta-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 Predicta-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. Delivery is at-least-once by nature: key on `dedupeKey` to make handling idempotent. The signature is `predicta-signature: t=<unix>,v1=<hmac-sha256 of \"t.body\">`, with a 300-second replay window; 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.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Any 2xx acknowledges. A 3xx is treated as a failure."
          }
        }
      }
    },
    "order.filled": {
      "post": {
        "summary": "An order filled. NOT YET DELIVERED.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "order.rejected": {
      "post": {
        "summary": "An order was rejected. NOT YET DELIVERED.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "position.updated": {
      "post": {
        "summary": "A position changed. NOT YET DELIVERED.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Acknowledged."
          }
        }
      }
    },
    "market.resolution_changed": {
      "post": {
        "summary": "A market’s resolution changed. NOT YET DELIVERED.",
        "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": {
      "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`."
          }
        }
      },
      "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": "Predicta’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": "Predicta’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"
          },
          "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 the book has no usable side: show “no price”, never the reference."
          },
          "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."
          },
          "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."
          },
          "volume24h": {
            "type": [
              "number",
              "null"
            ]
          },
          "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`."
          }
        }
      },
      "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"
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "A Predicta 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"
            ]
          },
          "volume24h": {
            "type": [
              "number",
              "null"
            ]
          },
          "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"
            ]
          },
          "stake": {
            "type": "number",
            "description": "Display. The exact figure is `operatorMoney.authorizationAmount`."
          },
          "predictaFee": {
            "type": "number",
            "description": "Display."
          },
          "predictaFeeBps": {
            "type": "number",
            "description": "The rate this quote was struck at, echoed so reconciliation asserts the terms agreed rather than what our config says today."
          },
          "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."
          }
        }
      },
      "Order": {
        "type": "object",
        "properties": {
          "orderId": {
            "type": "string"
          },
          "clientOrderId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "submitted",
              "partially_filled",
              "filled",
              "cancelled",
              "rejected"
            ],
            "description": "`submitted` is the UNRESOLVED state — the venue was asked and has not answered. It is not a failure."
          },
          "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"
            ]
          },
          "predictaFee": {
            "type": "number"
          },
          "predictaFeeBps": {
            "type": "number"
          },
          "notional": {
            "type": "number"
          },
          "stake": {
            "type": "number"
          },
          "rejectReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "simulated": {
            "type": "boolean"
          },
          "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."
          }
        }
      },
      "OrderDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Order"
          },
          {
            "type": "object",
            "properties": {
              "externalUserId": {
                "type": "string"
              },
              "settlementState": {
                "type": "string",
                "enum": [
                  "settled",
                  "pending"
                ],
                "description": "THE FIELD A RECOVERING INTEGRATION READS FIRST. `pending` means HOLD the authorization and poll; do not release."
              },
              "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": "Predicta’s stable id for this settlement. Safe as the idempotency key for a credit."
          },
          "positionId": {
            "type": "string"
          },
          "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"
          }
        }
      },
      "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 Predicta’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."
          },
          "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 Predicta 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 | FEE | SELL | SETTLEMENT | VOID_REFUND | ADJUSTMENT."
          },
          "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."
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "order.filled",
              "order.rejected",
              "position.updated",
              "position.settled",
              "market.resolution_changed"
            ]
          },
          "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"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      }
    }
  }
}
