> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trysoar.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Get everything about one market in a single call

> The endpoint to call first. It returns the market's metadata, its order
book, its last print, its activity, **and your own position and resting
orders in it** — the five reads you would otherwise issue separately and
have to join yourself.

Caller-scoped: `positions` and `resting_orders` are the authenticated
key's, never anyone else's.

Two things it deliberately does not include. Your cash balance is not
per-market — read `/portfolio/balance` once and cache it, rather than
re-fetching it per market. And the sibling rungs of a ladder are not
here either: follow `market.event_ticker` to
`GET /events/{event_ticker}` for the whole ladder in strike order.




## OpenAPI

````yaml https://api.trysoar.com/v1/openapi.yaml get /markets/{market_id}/snapshot
openapi: 3.1.0
info:
  title: SOAR Trading API
  version: 1.0.0
  description: >
    Trade prediction markets on private-company valuations.


    **Authentication.** Log in at https://app.trysoar.com, open Settings,

    choose Developer settings, and create an API key with the `read` and

    `trade` scopes. Every request carries exactly one credential: that key in

    `X-API-KEY` or an OAuth bearer token. API-key scopes do not nest: `read`

    covers market data and portfolio reads; `trade` covers placing, amending,

    or canceling orders. OAuth tokens use the granular scopes

    declared on each operation. A `trade`-only key is rejected on reads. A

    missing or unusable credential is `401`; a valid credential without the

    required scope is `403`.


    **Prices are whole cents, 1–99.** All amounts are integer cents. Fee rates

    and valuations are decimal *strings*, never JSON floats — a float64 loses

    precision at these valuations. Hash-derived identifiers (`order_id`) are

    strings because they exceed the range a JavaScript number represents

    exactly.


    **Enums are strings.** `outcome` is `yes`/`no`, `side` is `buy`/`sell`,

    `time_in_force` is `gtc`/`ioc`/`fok`. Requests accept them

    case-insensitively; responses always emit the lower-case string. A rejected

    value returns `400` whose message names every accepted value.


    ## Addressing markets


    **Every path that accepts a `market_id` also accepts the market's ticker**,

    case-insensitively (the column is `CITEXT`), and every response that

    identifies a market carries both — so you can adopt tickers incrementally

    rather than in one cutover.


    Tickers nest by prefix. A market ticker begins with its event ticker, and an

    event ticker begins with its series ticker:

        series   ANTHROPIC-VALUATION
        event    ANTHROPIC-VALUATION-26Q3        {series}-{YY}Q{n}, from the event's resolution date
        market   ANTHROPIC-VALUATION-26Q3-1.05T  {event}-{strike}, one ladder rung
                 STRP-PYPL-26Q4-YES              {event}-YES, a binary market

    So `series ⊂ event ⊂ market`. Two things follow, and both save you a lookup:

    an event's markets are exactly the tickers that start with the event ticker,

    and a market's ticker is its event ticker plus a strike segment. The tenor
    is

    quarter-granular (`26Q3`, never `26`) because ladder events roll quarterly.


    **Containment is a prefix guarantee, not a decomposition rule.** "Begins

    with" holds by construction. Splitting an arbitrary market ticker back into

    `{event}` + `{strike}` does **not**: nothing forbids a hyphen inside a
    strike

    label, so cutting on the last `-` relies on today's labels rather than on

    this contract. Use `event_ticker`, which every market, order and position

    carries.


    Two further cautions. **The series segment is assigned, not derived — read

    it, do not construct it.** A listed market's series is usually its company

    symbol plus the market category spelled in full (`ANTHROPIC-VALUATION`), but

    an operator may name a series explicitly, and then it is simply whatever

    precedes the tenor (`STRP-PYPL-26Q4` has series `STRP-PYPL`, which is
    neither

    symbol nor category). So take `series_ticker` from the response rather than

    assembling it, and never assume one company's series from another's. And

    **dots are legal and load-bearing**: most ladder strikes carry a decimal

    point (`OPENAI-VALUATION-26Q3-212.5B`), and `212.5B` and `2125B` are

    different rungs. Never sanitize a ticker to alphanumeric.


    ## Ordering, paging, and time


    Every timestamp is RFC3339 with nanosecond precision, UTC, `Z`-suffixed.


    List endpoints take `limit` (1–1000, default 100) and `cursor`, and return a

    `cursor` field that is **the empty string on the last page** — never absent,

    never null, so your loop condition is uniform. The cursor is an **opaque

    token**: do not parse, construct, or persist it across API versions. An

    unparseable cursor is `400 invalid_cursor`, never a silent restart from page

    one — silently restarting a paging loop is how a bot doubles positions.


    Empty collections are `[]`, never `null` — `bids`, `asks`, `candles`,

    `trades`, `markets`, `events`, `orders`, `positions`, `fills`,

    `resting_orders`. A brand-new market's snapshot is a well-formed object with

    empty arrays and an absent `last_trade`, not nulls and not a `500`. That is

    the common case for a new listing, and the first thing you will hit.


    ## Market data


    Start at **`GET /markets/{market_id}/snapshot`**. It returns the market, its

    order book, its last print, its activity, and your own position and resting

    orders in it — the five reads you would otherwise join yourself. The

    individual endpoints exist for when you want one of them on its own.


    To find something to trade: `GET /markets?q=openai` searches by company

    name, symbol, or ticker prefix. To see how markets relate: `GET /events`

    groups them into ladders and gives you the rungs in strike order, which a

    flat market list cannot.


    **Depth and bars are recent, not live.** Order-book snapshots are published

    asynchronously, so `orderbook.as_of` is when the depth was captured. Read
    it,

    and treat an `as_of` that stops advancing on an otherwise busy venue as a

    stalled feed rather than a quiet market. Order *writes*, by contrast, are

    synchronous:


    **Order writes are synchronous against the engine.** Matching has already

    happened when a create or amend returns, so the response carries the

    engine's own `filled_qty` and `remaining_qty`, and `status` reflects what

    actually happened — an IOC that filled nothing comes back `canceled`, not

    `accepted`. The *read* endpoints are projected asynchronously, so a read may

    briefly lag a write you just made.


    ## Errors


    Every error body carries a `code` from a closed set. **Branch on `code`.**

    The `error` field is free text for a human reading a log; it is not part of

    the contract and may change without notice. New codes may be added, so treat

    an unrecognized one as a generic failure of its HTTP class.


    **`result_unknown` (503) does not mean failure.** It means the command was

    produced but no reply arrived before the deadline — it **may have

    committed**. Do not retry blindly and do not treat it as a rejection. Either

    poll `GET /orders/{order_id}`, or re-send the identical request with the

    **same `client_order_id`**, which the engine deduplicates. That is what
    makes

    the retry safe.


    **Rate limit.** One token bucket per API key or stable OAuth principal
    across

    the whole surface: 250 requests/second sustained, 500 burst. Every response

    *from an authenticated request* carries `X-RateLimit-Limit` (the burst) and

    `X-RateLimit-Remaining` (the true post-decrement level). A `401` carries

    neither because authentication runs before the limiter. A rejection is `429`
    with `Retry-After` and

    `X-RateLimit-Reset`; those two headers are present ONLY on a `429`.
    Self-pace

    from `X-RateLimit-Remaining` rather than discovering the cap by hitting it —

    a throttled cancel leaves an order resting that you believe you pulled.
servers:
  - url: https://api.trysoar.com/v1
    description: Production
  - url: https://api.staging.trysoar.com/v1
    description: Staging
security:
  - ApiKeyAuth: []
paths:
  /markets/{market_id}/snapshot:
    get:
      summary: Get everything about one market in a single call
      description: |
        The endpoint to call first. It returns the market's metadata, its order
        book, its last print, its activity, **and your own position and resting
        orders in it** — the five reads you would otherwise issue separately and
        have to join yourself.

        Caller-scoped: `positions` and `resting_orders` are the authenticated
        key's, never anyone else's.

        Two things it deliberately does not include. Your cash balance is not
        per-market — read `/portfolio/balance` once and cache it, rather than
        re-fetching it per market. And the sibling rungs of a ladder are not
        here either: follow `market.event_ticker` to
        `GET /events/{event_ticker}` for the whole ladder in strike order.
      operationId: getMarketSnapshot
      parameters:
        - $ref: '#/components/parameters/MarketRef'
      responses:
        '200':
          description: The market and your state in it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Snapshot'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '404':
          description: No such market.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - soar:account:read
components:
  parameters:
    MarketRef:
      name: market_id
      in: path
      required: true
      description: |
        The market's ticker **or** its `event_market_id` UUID. Tickers are
        case-insensitive.
      schema:
        type: string
        pattern: ^[A-Za-z0-9.](?:[A-Za-z0-9.-]*[A-Za-z0-9.])?$
        examples:
          - ANTHROPIC-VALUATION-26Q3-1.05T
  schemas:
    Snapshot:
      type: object
      required:
        - market
        - orderbook
        - stats
        - positions
        - resting_orders
      properties:
        market:
          $ref: '#/components/schemas/Market'
        orderbook:
          $ref: '#/components/schemas/Orderbook'
        last_trade:
          allOf:
            - $ref: '#/components/schemas/MarketTrade'
          description: |
            The most recent print. **Absent** when the market has never traded —
            not a zero-valued object, which would read as "last printed at 0".
        stats:
          $ref: '#/components/schemas/MarketStats'
        positions:
          type: array
          description: |
            **Your** holdings in this market, `[]` when you hold none.

            An array, not a single object, and that matters: you can hold YES
            and NO in the same market at once, so one market can produce two
            entries. Code that reads only the first drops a leg of its own
            hedge.
          items:
            $ref: '#/components/schemas/Position'
        resting_orders:
          type: array
          description: |
            **Your** still-working orders in this market — the `resting` status
            group. `[]` when you have none.

            Together with `positions` this also gives you your free inventory,
            which no single endpoint reports: your sell-side `remaining_qty`
            here is the part of your position that is already committed.
          items:
            $ref: '#/components/schemas/Order'
      description: |
        Everything you need to decide on one market, in one call.

        **Sections fail differently, on purpose.** Market colour — the order
        book and the last print — degrades to an empty value if its backing
        store is unavailable, because four of five sections beat none, and
        `orderbook.as_of` already tells you the depth is not fresh. Your own
        state never degrades: if positions or resting orders cannot be read the
        whole call fails, because an empty `resting_orders` you believe reads as
        "nothing working here", and a quoter that believes that re-quotes on top
        of its own live orders.

        A brand-new market — no book, no trades, no position, no orders — is a
        well-formed object with empty arrays and an absent `last_trade`. That is
        the common case for a new listing, not an error.
    Error:
      type: object
      required:
        - code
        - error
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        error:
          type: string
          description: |
            Human-readable detail for logs. **Not part of the contract** and may
            change without notice. Never match on this string.
        details:
          type: string
          description: Optional additional context. May be absent.
    Market:
      type: object
      required:
        - ticker
        - market_id
        - event_ticker
        - project_market_id
        - name
        - outcome_label
        - state
        - min_price
        - max_price
        - tick_size
        - maker_fee_rate
        - taker_fee_rate
        - open_interest
        - created_at
        - updated_at
      properties:
        ticker:
          type: string
        market_id:
          type: string
          format: uuid
        event_ticker:
          type: string
        project_market_id:
          type: string
          format: uuid
          description: The parent prediction market — the ladder a rung belongs to.
        name:
          type: string
        description:
          type: string
          description: Absent when unset.
        outcome_label:
          type: string
        state:
          type: string
          enum:
            - active
            - halted
            - resolved
        min_price:
          type: integer
        max_price:
          type: integer
        tick_size:
          type: integer
        strike_value:
          type: string
          description: >
            The rung's valuation threshold as a decimal **string**, not a
            number:

            a float64 loses precision here, and a $16B strike has been observed

            round-tripping as 15999999999.999998. Absent on binary markets,
            which

            genuinely have no strike; present on every ladder rung.
          examples:
            - '1050000000000.00'
        strike_unit:
          type: string
        maker_fee_rate:
          type: string
          description: >
            Decimal **string**, and a FRACTION not a percent — `"0.0700"` means

            7%. Fees follow a Kalshi-style parabolic curve, `rate x qty x p x

            (1-p)`, ceiled to whole cents and computed on the **original
            economic

            price** rather than the canonical YES price. The current denominator

            is deliberately scaled down for the paper-money beta; it is not the

            eventual production rate.
          examples:
            - '0.0700'
        taker_fee_rate:
          type: string
          examples:
            - '0.0700'
        last_price:
          type: integer
          description: Absent when the market has never traded.
        open_interest:
          type: integer
          minimum: 0
          description: |
            Contracts outstanding — covered YES+NO pairs issued and not yet
            retired. Same field as `MarketStats.open_interest`; see there for
            the full definition.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Orderbook:
      type: object
      required:
        - ticker
        - market_id
        - as_of
        - bids
        - asks
      properties:
        ticker:
          type: string
        market_id:
          type: string
          format: uuid
        as_of:
          type: string
          format: date-time
          description: |
            When this depth was captured — **not** when you asked for it.

            Depth is captured on a short interval and published
            asynchronously, so this is a *recent* view rather than a live one.
            Size against it accordingly.

            **Watch this field.** An `as_of` that stops advancing while trades
            keep printing means the depth feed is stale, not that the market has
            gone quiet — the two look identical if you read only the levels. A
            market that has never traded has no snapshot at all and reports the
            zero instant with empty sides.
        bids:
          type: array
          description: Best first. `[]` on a book with no bids — never null.
          items:
            $ref: '#/components/schemas/OrderbookLevel'
        asks:
          type: array
          description: Best first. `[]` on a book with no asks — never null.
          items:
            $ref: '#/components/schemas/OrderbookLevel'
    MarketTrade:
      type: object
      required:
        - trade_id
        - ticker
        - market_id
        - price
        - qty
        - created_at
      properties:
        trade_id:
          type: string
          description: |
            **Opaque.** An equality key for deduplicating overlapping pages —
            it carries no ordering, so do not parse it, compare it as a number,
            or sort by it. Order by `created_at`.
        ticker:
          type: string
        market_id:
          type: string
          format: uuid
        price:
          type: integer
          minimum: 1
          maximum: 99
          description: |
            The print in canonical (YES) cents. Both counterparties see this
            same number — it is the tape price and the basis candles are built
            from. It is the `trade_price` of `Fill`; there is no
            `outcome_price` here because a public tape has no "your side".
        qty:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
      description: |
        One print on the public tape.

        **No counterparty or order identity is published**, on any trade: a
        market-wide feed readable by any key that also named the participants
        would let anyone fingerprint other traders' flow. Your own fills, with
        your order ids and your side's economics, are on `/portfolio/fills`.
    MarketStats:
      type: object
      required:
        - volume_usd
        - open_interest
      properties:
        volume_usd:
          type: string
          description: |
            **Lifetime** cumulative traded notional, as a decimal string of
            dollars. Not a 24-hour figure — the only money field on this API
            that is dollars rather than integer cents, because the stored
            running total is.

            There is deliberately no `volume_24h` here. Nothing in the venue
            maintains one, and publishing a hard zero as "24h volume" for a
            market that traded all day is worse than publishing nothing: you
            could not tell it from a dead market. For a windowed figure, sum
            `volume` across `GET /markets/{market_id}/candles` over the window
            you care about — that is aggregated per bucket from the tape.
        open_interest:
          type: integer
          minimum: 0
          description: |
            Contracts outstanding — the number of covered YES+NO pairs the
            market currently has issued and not yet retired. Counted in
            CONTRACTS, not shares: one contract is one pair, so a market with
            `open_interest: 4200` has 4,200 YES shares and 4,200 NO shares
            outstanding.

            Derived from the collateral the market holds, not from the tape.
            Issuing a pair locks 100¢ and retiring one releases 100¢, so this
            moves on issuance or retirement and NOT on a transfer between
            holders — two people swapping an existing contract changes volume,
            not open interest. A resolved market reads 0.
        last_price:
          type: integer
          minimum: 1
          maximum: 99
          description: >-
            Last print in canonical YES cents. Absent when the market has never
            traded.
    Position:
      type: object
      required:
        - ticker
        - market_id
        - event_ticker
        - outcome
        - qty
        - cost_basis
        - avg_entry_price
        - realized_pnl
      properties:
        ticker:
          type: string
        market_id:
          type: string
          format: uuid
        event_ticker:
          type: string
        outcome:
          $ref: '#/components/schemas/Outcome'
        qty:
          type: integer
          minimum: 0
          description: >
            **Never negative, on any market.** Since the two-column unification
            a

            ladder rung carries `[yes, no]` exactly as a binary market does, so

            there are no signed holdings anywhere. Long NO is a NO position, not

            a negative YES one — code that treats a negative as "short" is

            handling an unreachable case and will mis-size if it ever fires.
        cost_basis:
          type: integer
        avg_entry_price:
          type: integer
        realized_pnl:
          type: integer
    Order:
      type: object
      required:
        - order_id
        - ticker
        - market_id
        - event_ticker
        - outcome
        - side
        - price
        - qty
        - remaining_qty
        - filled_qty
        - status
        - time_in_force
        - created_at
        - updated_at
      properties:
        order_id:
          type: string
          pattern: ^[0-9]{1,20}$
          description: |
            Decimal digits in a string. It is FNV-derived and routinely exceeds
            2^53, so a JSON number would be silently rounded by any JavaScript
            client and you would then cancel an order that does not exist.
        client_order_id:
          type: string
          maxLength: 64
          description: |
            Echoed on the create response when you supplied one. **Not stored,
            so it is absent from every read** (`GET /orders`, `GET
            /orders/{order_id}`) — keep your own mapping. `order_id` is derived
            deterministically from it, so re-sending the identical create is
            also a safe way to recover the id.
        ticker:
          type: string
          description: This market's ticker.
        market_id:
          type: string
          format: uuid
        event_ticker:
          type: string
          description: >-
            The parent event's ticker — the prefix this market's ticker begins
            with.
        outcome:
          $ref: '#/components/schemas/Outcome'
        side:
          $ref: '#/components/schemas/Side'
        price:
          type: integer
          minimum: 1
          maximum: 99
          description: Your limit price in whole cents, for your own outcome.
        qty:
          type: integer
          minimum: 0
          description: Original size in shares.
        remaining_qty:
          type: integer
          minimum: 0
        filled_qty:
          type: integer
          minimum: 0
        status:
          $ref: '#/components/schemas/OrderStatus'
        reserved_cents:
          type: integer
          minimum: 0
          description: |
            Cash locked by **this order**. Present on reads only — a create or
            amend response omits it, because the engine reports the caller's
            account-wide reserve there rather than this order's share, and one
            field name cannot mean both. For the account-wide figure use
            `reserved_cents` on `/portfolio/balance`.
        time_in_force:
          $ref: '#/components/schemas/TimeInForce'
        spent_cents:
          type: integer
          minimum: 1
          description: |
            Present only on the **create response of a budget order**: the exact
            realized cash cost of the execution — fill flow plus taker fees,
            cent for cent what left your balance. Absent on qty orders and on
            every read (the projection does not store it); reconcile historical
            executions against `/portfolio/fills`. On a budget order the
            executed size is `qty` (= `filled_qty`), so `spent_cents / qty` is
            your realized average cost per share, fees included.
        created_at:
          type: string
          format: date-time
          description: >
            On a **read**, when the engine sequenced the order. On a **create or

            amend response**, the gateway's submit instant instead — the
            engine's

            reply carries no timestamp, and the authoritative one is only
            visible

            to the projection. The two are within a millisecond, but they are
            not

            the same clock: re-sending an idempotent `client_order_id` returns
            the

            same order with a fresh `created_at`. Key on `order_id`, not on
            this.
        updated_at:
          type: string
          format: date-time
    ErrorCode:
      type: string
      description: |
        Closed, machine-branchable failure identifier. Branch on this rather
        than on `error` text.
      enum:
        - invalid_request_body
        - invalid_market_identifier
        - invalid_order_id
        - invalid_price
        - invalid_qty
        - invalid_budget
        - invalid_enum_value
        - invalid_cursor
        - invalid_parameter
        - conflicting_identifiers
        - conflicting_credentials
        - missing_idempotency_key
        - unauthenticated
        - forbidden_scope
        - api_key_misconfigured
        - market_not_found
        - order_not_found
        - insufficient_balance
        - insufficient_shares
        - market_halted
        - market_resolved
        - duplicate_order_id
        - order_not_cancelable
        - budget_not_fillable
        - no_executable_liquidity
        - invalid_request
        - venue_halted
        - venue_cancel_only
        - rate_limited
        - engine_overloaded
        - engine_unavailable
        - result_unknown
        - internal_error
    OrderbookLevel:
      type: object
      required:
        - price
        - size
      properties:
        price:
          type: integer
          minimum: 1
          maximum: 99
          description: Whole cents, in canonical YES terms.
        size:
          type: integer
          minimum: 0
          description: Whole shares resting at this level.
    Outcome:
      type: string
      enum:
        - 'yes'
        - 'no'
        - unknown
      description: |
        Which side of the market. Your position is in this outcome.

        `unknown` is emitted only for a stored value this API version does not
        recognise (corruption, or an encoding added after your client was
        generated). It is declared so one bad row cannot fail the whole page;
        treat it as unparseable data rather than a fourth outcome.
    Side:
      type: string
      enum:
        - buy
        - sell
        - unknown
      description: See `Outcome` for when `unknown` appears.
    OrderStatus:
      type: string
      enum:
        - accepted
        - open
        - partially_filled
        - filled
        - canceled
        - expired
      description: |
        `accepted` is a transient projection state visible only on **reads**,
        while the durable state catches up. A create or amend response never
        returns it: its status is derived from what the engine actually did.
    TimeInForce:
      type: string
      enum:
        - gtc
        - ioc
        - fok
        - unknown
      description: |
        `gtc` rests until filled or canceled. `ioc` fills what it can
        immediately and cancels the rest. `fok` fills entirely or not at all.
        Omitted means `gtc` — an order that would silently become `ioc` dies the
        moment it fails to cross.
  responses:
    Unauthenticated:
      description: |
        Missing or unusable credential. Bearer failures include the protected
        resource metadata URL in `WWW-Authenticate`; API-key failures do not.
      headers:
        WWW-Authenticate:
          description: OAuth Bearer challenge when applicable.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unauthenticated
            error: API key or bearer token required
    ForbiddenScope:
      description: |
        The credential authenticated but lacks the required scope. OAuth
        failures include an `insufficient_scope` Bearer challenge.
      headers:
        WWW-Authenticate:
          description: OAuth Bearer challenge when applicable.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: forbidden_scope
            error: 'api key lacks scope: trade'
    RateLimited:
      description: >
        Per-principal bucket exhausted, or the engine applied backpressure.

        Honor `Retry-After`. Both mean nothing was sequenced, so a retry is
        safe.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limited
            error: rate limited (per-api-key)
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: |
        Per-user API key. Create and revoke keys from your SOAR account
        settings. A revoked key stops working within 60 seconds.
    OAuth2:
      type: oauth2
      description: >-
        Authorization code with PKCE S256. Tokens are audience-bound to this
        API.
      flows:
        authorizationCode:
          authorizationUrl: /oauth/authorize
          tokenUrl: /oauth/token
          scopes:
            soar:markets:read: Read market catalog, ladders, books, candles, and trades.
            soar:account:read: >-
              Read orders, fills, balances, positions, and personalized
              snapshots.
            soar:orders:write: Place, amend, or cancel one known order.
            soar:orders:bulk-cancel: Cancel fan-out through the trusted SOAR MCP workload.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.