> ## 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 the public trade tape for a market

> Every print in one market, newest first — not just yours. Use `limit` to
size the tail; deduplicate overlapping reads on `trade_id`.

For your own executions, with your order ids and the price that
reconciles against your cash, use `/portfolio/fills` instead.




## OpenAPI

````yaml https://api.trysoar.com/v1/openapi.yaml get /markets/{market_id}/trades
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}/trades:
    get:
      summary: Get the public trade tape for a market
      description: |
        Every print in one market, newest first — not just yours. Use `limit` to
        size the tail; deduplicate overlapping reads on `trade_id`.

        For your own executions, with your order ids and the price that
        reconciles against your cash, use `/portfolio/fills` instead.
      operationId: getMarketTrades
      parameters:
        - $ref: '#/components/parameters/MarketRef'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: A page of the tape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketTradesPage'
        '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:markets: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
    Limit:
      name: limit
      in: query
      required: false
      description: Page size. Values outside 1–1000 are clamped, never rejected.
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 100
  schemas:
    MarketTradesPage:
      type: object
      required:
        - trades
        - cursor
      properties:
        trades:
          type: array
          description: Newest first. `[]` on a market that has never traded.
          items:
            $ref: '#/components/schemas/MarketTrade'
        cursor:
          type: string
          description: |
            Always empty. The tape is meant to be tailed with `limit` rather
            than paged deeply; the field is present so your paging loop reads
            the same shape here as everywhere else.
    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.
    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`.
    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
  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.