> ## 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.

# List events and their ladder shape

> An **event** is one prediction market — a survival ladder, or a single
binary — and its `markets` are that event's rungs.

This is the endpoint that tells you which markets belong together. `GET
/markets` is a flat list and cannot: two rungs of the same ladder look
no different there from two unrelated markets. Start here when you want
to trade a distribution rather than a single threshold.




## OpenAPI

````yaml https://api.trysoar.com/v1/openapi.yaml get /events
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:
  /events:
    get:
      summary: List events and their ladder shape
      description: |
        An **event** is one prediction market — a survival ladder, or a single
        binary — and its `markets` are that event's rungs.

        This is the endpoint that tells you which markets belong together. `GET
        /markets` is a flat list and cannot: two rungs of the same ladder look
        no different there from two unrelated markets. Start here when you want
        to trade a distribution rather than a single threshold.
      operationId: listEvents
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: series_ticker
          in: query
          required: false
          description: |
            Scope to one series — every quarterly roll of one ladder.
            Case-insensitive.
          schema:
            type: string
            examples:
              - ANTHROPIC-VALUATION
        - name: project_id
          in: query
          required: false
          description: Scope to one company. Get it from `Event.project_id`.
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          required: false
          description: >-
            Scope to one lifecycle state. Unknown values are `400` naming the
            set.
          schema:
            $ref: '#/components/schemas/EventStatus'
      responses:
        '200':
          description: A page of events.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventsPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/ForbiddenScope'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - soar:markets:read
components:
  parameters:
    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
    Cursor:
      name: cursor
      in: query
      required: false
      description: |
        Opaque continuation token from the previous page's `cursor`. Omit for
        the first page. Do not parse or construct one.
      schema:
        type: string
  schemas:
    EventStatus:
      type: string
      enum:
        - unopened
        - open
        - closed
        - settled
      description: |
        Lifecycle of the event as a whole. `settled` means every rung has
        resolved — a ladder mid-resolution is still `closed`, because its
        remaining rungs still settle independently.
    EventsPage:
      type: object
      required:
        - events
        - cursor
      properties:
        events:
          type: array
          description: Ordered by event ticker. `[]` when nothing matches — never null.
          items:
            $ref: '#/components/schemas/Event'
        cursor:
          type: string
          description: Empty string on the last page.
    Event:
      type: object
      required:
        - event_ticker
        - series_ticker
        - project_market_id
        - project_id
        - title
        - category
        - status
        - mutually_exclusive
        - resolution_date
        - markets
      properties:
        event_ticker:
          type: string
          examples:
            - ANTHROPIC-VALUATION-26Q3
        series_ticker:
          type: string
          description: |
            The series this event rolls in. Events roll **quarterly**, so the
            series plus a tenor is what identifies one of them.
          examples:
            - ANTHROPIC-VALUATION
        project_market_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
          description: The company. Pass it back as `project_id` on `GET /markets`.
        title:
          type: string
        sub_title:
          type: string
          description: Absent when unset.
        category:
          type: string
        status:
          $ref: '#/components/schemas/EventStatus'
        mutually_exclusive:
          type: boolean
          description: >
            `true` for a mutex set (a binary YES/NO market, or a multi-candidate

            field) — exactly one outcome pays. `false` for a **survival
            ladder**,

            where the rungs are nested: if the $2.1T rung resolves YES then
            every

            lower rung did too.
        resolution_source:
          type: string
          description: Absent when unset.
        resolution_date:
          type: string
          format: date-time
        markets:
          type: array
          description: |
            The event's rungs, **ordered by `strike_value` ascending**, with
            strikeless rungs (a binary market's single market) last.

            The order is the contract, not a convenience. A survival ladder
            encodes P(value > strike), monotone decreasing in strike, and you
            cannot read that shape out of an insertion-ordered list. Adjacent
            rung prices are what give you the implied probability mass in each
            band.

            Every rung is listed, including resolved ones — the ladder's
            structure is the point, and `state` on each market tells you which
            are still tradeable.
          items:
            $ref: '#/components/schemas/Market'
    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
    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)
  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.