> ## 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 OHLCV bars for a market

> Bars over `[from, to]`, oldest first, defaulting to the last 24 hours.

Built from the trade tape, so prices are in canonical YES terms. Unlike
the order book this is not a live view either — it is a rollup of what
already printed.




## OpenAPI

````yaml https://api.trysoar.com/v1/openapi.yaml get /markets/{market_id}/candles
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}/candles:
    get:
      summary: Get OHLCV bars for a market
      description: |
        Bars over `[from, to]`, oldest first, defaulting to the last 24 hours.

        Built from the trade tape, so prices are in canonical YES terms. Unlike
        the order book this is not a live view either — it is a rollup of what
        already printed.
      operationId: getCandles
      parameters:
        - $ref: '#/components/parameters/MarketRef'
        - name: interval
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/CandleInterval'
        - name: from
          in: query
          required: false
          description: RFC3339 start of the window. Defaults to 24 hours ago.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: |
            RFC3339 end of the window. Defaults to now. A `from` after `to` is
            `400 invalid_parameter`, not an empty series — you cannot tell a
            mistyped range from a quiet market, and one of those you can fix.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: The bar series.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandlesPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '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'
        '503':
          $ref: '#/components/responses/MarketDataUnavailable'
      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
  schemas:
    CandleInterval:
      type: string
      enum:
        - 30s
        - 1m
        - 1h
        - 4h
        - 12h
        - 1d
      default: 1m
      description: |
        Bar width. `30s`, `1m`, `1h` and `1d` are stored rollups; `4h` and `12h`
        are re-aggregated from the hourly rollup at query time and cost slightly
        more. There is deliberately no other value: an interval with no backing
        rollup would return an empty series, and you could not tell that from a
        quiet market — so anything else is `400` naming the set.
    CandlesPage:
      type: object
      required:
        - ticker
        - market_id
        - interval
        - candles
      properties:
        ticker:
          type: string
        market_id:
          type: string
          format: uuid
        interval:
          $ref: '#/components/schemas/CandleInterval'
        candles:
          type: array
          description: Oldest first. `[]` when nothing traded in the window — never null.
          items:
            $ref: '#/components/schemas/Candle'
    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.
    Candle:
      type: object
      required:
        - timestamp
        - open
        - high
        - low
        - close
        - volume
        - trade_count
      properties:
        timestamp:
          type: string
          format: date-time
          description: Start of the bucket, calendar-aligned.
        open:
          type: integer
          minimum: 1
          maximum: 99
        high:
          type: integer
          minimum: 1
          maximum: 99
        low:
          type: integer
          minimum: 1
          maximum: 99
        close:
          type: integer
          minimum: 1
          maximum: 99
        volume:
          type: integer
          minimum: 0
          description: Shares traded in the bucket.
        trade_count:
          type: integer
          minimum: 0
      description: >
        OHLC is in canonical YES cents — the same basis as `trade_price` on the

        tape these bars are built from. A bucket with no trades carries the

        previous close forward on all four prices, with `volume` and

        `trade_count` at `0`; the series is continuous by construction, so a
        flat

        run of bars means no trades, not missing data.
    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:
    BadRequest:
      description: |
        The request is malformed or self-contradictory. Branch on `code`; the
        message names what would have worked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: invalid_enum_value
            error: side must be one of "buy", "sell", got "hold"
    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)
    MarketDataUnavailable:
      description: |
        The time-series store backing this read is not configured or not
        reachable. Trading is unaffected — order placement, cancellation and
        portfolio reads do not depend on it — so degrade to quoting without
        depth rather than halting.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: engine_unavailable
            error: market data store not configured
  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.