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

# Place an order

> Matching has already happened when this returns: the response carries
the engine's own `filled_qty`, `remaining_qty`, and a `status` derived
from what actually happened. An `ioc` or `fok` that filled nothing comes
back `canceled`.

Size the order in **shares** (`qty`) or in **cash** (`budget_cents`),
never both. A budget order is a taker buy — "spend $50.00 on yes" — that
the engine sizes against the live book inside the match step, fees
included, with `price` as your worst-price bound; see
`CreateOrderRequest.budget_cents` for the full semantics.

`client_order_id` is your idempotency key and is scoped to your user. The
gateway derives the engine's `command_id` and the `order_id`
deterministically from it, so re-sending the same one returns the same
order — which is exactly what makes it the safe response to a
`result_unknown` 503.




## OpenAPI

````yaml https://api.trysoar.com/v1/openapi.yaml post /orders
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:
  /orders:
    post:
      summary: Place an order
      description: >
        Matching has already happened when this returns: the response carries

        the engine's own `filled_qty`, `remaining_qty`, and a `status` derived

        from what actually happened. An `ioc` or `fok` that filled nothing comes

        back `canceled`.


        Size the order in **shares** (`qty`) or in **cash** (`budget_cents`),

        never both. A budget order is a taker buy — "spend $50.00 on yes" — that

        the engine sizes against the live book inside the match step, fees

        included, with `price` as your worst-price bound; see

        `CreateOrderRequest.budget_cents` for the full semantics.


        `client_order_id` is your idempotency key and is scoped to your user.
        The

        gateway derives the engine's `command_id` and the `order_id`

        deterministically from it, so re-sending the same one returns the same

        order — which is exactly what makes it the safe response to a

        `result_unknown` 503.
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
            examples:
              limit-order:
                summary: Qty-denominated limit order
                value:
                  ticker: ANTHROPIC-VALUATION-26Q3-1.05T
                  outcome: 'yes'
                  side: buy
                  price: 42
                  qty: 100
                  time_in_force: gtc
                  client_order_id: quoter-a-0007
              budget-buy:
                summary: Budget-denominated taker buy ($50.00, worst price 45¢)
                value:
                  ticker: ANTHROPIC-VALUATION-26Q3-1.05T
                  outcome: 'yes'
                  side: buy
                  price: 45
                  budget_cents: 5000
                  time_in_force: fok
                  client_order_id: sizer-b-0001
      responses:
        '200':
          description: The engine applied the order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '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'
        '422':
          $ref: '#/components/responses/PreTradeRejected'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ResultUnknown'
      security:
        - ApiKeyAuth: []
        - OAuth2:
            - soar:orders:write
components:
  schemas:
    CreateOrderRequest:
      type: object
      required:
        - outcome
        - side
        - price
        - client_order_id
      properties:
        ticker:
          type: string
          description: The market, by ticker. Send this **or** `market_id`, not both.
        market_id:
          type: string
          format: uuid
          description: The market, by UUID. Send this **or** `ticker`, not both.
        outcome:
          $ref: '#/components/schemas/Outcome'
        side:
          $ref: '#/components/schemas/Side'
        price:
          type: integer
          minimum: 1
          maximum: 99
          description: |
            Limit price in whole cents, for the outcome you named. On a budget
            order this is your **worst acceptable per-share price** — a pure
            slippage bound; fills execute at the resting prices, which may be
            better.
        qty:
          type: integer
          minimum: 1
          description: |
            Whole shares. Send this **or** `budget_cents`, not both — an order
            is denominated in shares or in cash, never both at once.
        budget_cents:
          type: integer
          minimum: 1
          description: |
            A budget-denominated taker **buy**: "spend this many cents" instead
            of "buy this many shares". The engine sizes the order against the
            live book inside the match step, spending up to
            `min(budget_cents, your available cash)` **including taker fees** —
            there is no client-side dollars→shares conversion, so the budget is
            consumed exactly (to within one share at your price bound).

            Requires `side: buy` and `time_in_force` `ioc` or `fok` — a budget
            order never rests, because its derived size is only meaningful
            against the book it was sized on. `fok` fills the full affordable
            size or rejects `422 budget_not_fillable` when the book cannot
            absorb the budget within your price bound; `ioc` executes whatever
            is eligible and stops. The response's `qty`/`filled_qty` carry the
            engine-derived executed size and `spent_cents` the exact realized
            cost.
        time_in_force:
          $ref: '#/components/schemas/TimeInForce'
          description: Defaults to `gtc`. Budget orders must be `ioc` or `fok`.
        client_order_id:
          type: string
          minLength: 1
          maxLength: 64
          description: >
            Your idempotency key: 1–64 printable ASCII characters, **unique per

            user** (two users may safely use the same value). Re-sending the
            same

            one returns the same order rather than placing a second, which is

            what makes it the correct response to a `result_unknown` 503.
      description: |
        Supplying both `ticker` and `market_id` is `400 conflicting_identifiers`
        rather than a guessed winner — a caller that sends two identifiers does
        not know which one it means, and guessing on a trading request is the
        wrong kind of helpful. The same rule sizes the order: exactly one of
        `qty` (shares) or `budget_cents` (cash), and both at once is
        `400 invalid_budget`.
    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
    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.
    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.
    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.
    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.
    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'
    PreTradeRejected:
      description: |
        Well-formed, but a pre-trade check or a state gate rejected it —
        insufficient balance or shares, a halted or resolved market, or an order
        no longer in an actionable state.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: insufficient_balance
            error: insufficient balance
    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)
    ResultUnknown:
      description: |
        The command was produced but no reply arrived before the deadline. It
        **may have committed**. Do not treat it as a failure and do not retry
        blindly: poll the order, or re-send with the same `client_order_id`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: result_unknown
            error: engine result unknown
  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.