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

# Submit a new order

> Submits a custodial order: Kairos signs and routes it to the venue on the caller's behalf (custodial signing). This is the standard order-entry path used by both the web app and API-key consumers who have not been allow-listed onto the self-custody "external-signing" lane (`POST /v2/orders/intent` + `POST /v2/orders/submit`).
**Ownership.** The order is always created for the authenticated caller; any `user_id` in the body is accepted for backwards compatibility but is ignored — the JWT/API-key identity wins.
**Ack semantics (async).** A 200 response means the order was validated, persisted, and enqueued — it does NOT mean the order is live on the venue yet. The response `status` is `"queued"` (or, on an idempotent replay, the current status of the previously-created order). Track the order via `GET /orders/{order_id}`, `GET /orders` polling, or (lowest latency) the `/ws` socket, which pushes `order_update` / `fill` events as the worker submits to the venue and fills arrive.
**Idempotency.** Pass `client_order_id` to make retries safe: a second submit with the same `(user, exchange_id, market_id, client_order_id)` tuple returns the existing order instead of creating a duplicate, and does **not** consume a rate-limit slot. If omitted, the server generates a random one (i.e. retries without a client-supplied id are NOT deduped).
**Provider routing.** `exchange_id` selects the venue-specific executor (`polymarket`, `kalshi`, `predictfun`, `opinion`, `hyperliquid`, …; `kalshi_offchain` is a deprecated alias of `kalshi`). Each venue advertises its own capabilities (supported `time_in_force` values, max price, order types) — a request that is well-formed in general but unsupported by the specific venue is rejected with `400 EXCHANGE_UNSUPPORTED` / `400 VALIDATION_INVALID_ORDER`.
**Hyperliquid HIP-4.** Use the numeric outcome id as `market_id`, the selected display label as `outcome`, and the side coin from the market-data `OrderbookSnapshot.token_ids` as `token_id` (for example `#1010` or `#1011`). Quantities are whole shares. Hyperliquid maps `GTC`/`GTD` to venue GTC and `IOC`/`FAK`/`FOK` to venue IOC. Single-order and selected-order batch cancellation are supported; cancel-all and fee quotes are unavailable.
**Order types.** `kind="market"` is a marketable taker order — pair it with `time_in_force` `FOK` (fill-or-kill) or `FAK`/`IOC` (fill-and-kill / immediate-or-cancel, partials allowed, remainder cancelled). `kind="limit"` is a resting quote — pair it with `GTC` (good-til-cancelled) or `GTD` (good-til-date, requires `expiration_minutes`). `price` is REQUIRED for every order, including market orders — for a marketable order it is the limit you are willing to cross to, not a market-price sentinel.
**Circuit breakers & gating.** Before persisting, the order passes through, in order: the per-user order rate limit, size/price bounds, the venue time-in-force capability gate, a global + per-exchange execution kill-switch, the custodial trading-enabled check, the post-only capability gate, side/kind/TIF-specific breakers, and the API-key minimum-notional rule. Kill-switch and restriction rejections are `503` with `error_details.code = MARKET_PAUSED`; a disabled or identity-less account is `403` with `AUTH_CREDENTIALS_INVALID`. Designated canary users bypass both breaker checks.
**Rate limiting.** A Redis sliding-window limiter caps order submissions per authenticated user (default 5 orders per second, `ORDER_RATE_LIMIT_PER_SEC`). API keys with an `orders` override are checked against BOTH their own per-credential window and the aggregate per-user window (either denying is a `429`); idempotent replays are resolved before the limiter and never consume a slot. The limiter fails closed — an unreachable Redis denies with `429`. The `429` body carries `error_details.code = VALIDATION_INVALID_ORDER` with the message `Order rate limit exceeded` (there is no dedicated rate-limit code on this path), and no `Retry-After` header.
**Auth & scope.** Requires `trade:execute`. This mutation endpoint is additionally gated by a service-token/CSRF/API-key check (`RequireServiceToken`), which any request bearing `X-Client-Id` / `X-Api-Key` / `X-Api-Secret` satisfies automatically — no extra header is needed for API-key consumers.

**Before you submit**

* `price` is required on **every** order, including market orders — it is the limit you are willing to cross, not a sentinel.
* `quantity` must be positive. API-key BUYs also have a minimum notional.
* Prefer the [WebSocket order execution guide](/websocket/order-execution): one persistent connection, one round trip, and pushed fills instead of polling.


## OpenAPI

````yaml /openapi/execution.yaml post /orders
openapi: 3.1.0
info:
  title: Kairos Order Execution API
  version: 1.0.0
  summary: >-
    Order entry, cancellation, fee quotes, and live position exposure across
    venues.
  description: >
    The Order Execution API at `execution.kairos.trade` places and manages

    orders across every venue Kairos integrates. Two lanes:


    - **Custodial** (`POST /orders`) — Kairos signs and routes on your behalf
      (custodial signing). Async ack: a 200 means validated + enqueued; track via
      `GET /orders/{order_id}` or the WebSocket order/fill stream.
    - **Self-custody / external signing** (`POST /v2/orders/intent` +
      `POST /v2/orders/submit`) — for allow-listed institutional accounts on
      Polymarket and Predict.fun: Kairos builds the EIP-712 payload, you sign
      with your own key, submission is synchronous with the venue's verbatim
      result. `POST /v2/onchain/intent` + `POST /v2/onchain/submit` are the
      same pattern for on-chain operations (approvals, redeem, split/merge,
      unwrap) on Polygon, where you also pay the gas.

    ## Authentication


    All endpoints require a Kairos API key (`X-Client-Id` + `X-Api-Key` +

    `X-Api-Secret`) or a first-party session JWT, plus the per-operation

    scope (`trade:execute`, `trade:read`, `position:read`) where the operation

    documents one. Synthetic Books accepts any otherwise-valid account

    credential without an extra scope or allowlist. There is

    no anonymous tier. Scope checks apply to API-key credentials only; a

    session JWT carries no scope list and is treated as holding every scope.


    The custodial mutation endpoints (`POST /orders`, the three cancel

    endpoints) and Synthetic Books create/refresh/release carry an additional

    `RequireServiceToken` gate satisfied by **any** of: the full API-key triple,

    an internal `X-Service-Token`, or a valid `X-Csrf-Token`. API-key consumers

    need no extra header — but a bare `Authorization: Bearer <jwt>` alone is NOT

    sufficient on those routes. The `/v2/orders/*` and `/v2/onchain/*`

    external-signing routes have no such gate: a session JWT alone works there.


    ## Rate limits


    Order submission is capped per user (default 5 orders per **second**,

    sliding window, `ORDER_RATE_LIMIT_PER_SEC`); some credentials carry an

    `orders` override, which is enforced on its own per-credential window of

    the same duration *in addition to* the per-user window — either one

    denying is a `429`. Idempotent replays (same `client_order_id`) return the

    existing order before the limiter runs and never consume a slot. The

    limiter is Redis-backed and **fails closed**: if Redis is unreachable the

    submission is denied with `429`.


    Separately, repeated *authentication failures* from one client IP are

    throttled at 10 failures per 60 s (also fail-closed). That limiter returns

    the minimal `{"error": "Too many authentication attempts"}` body.


    **No rate-limit headers.** This service does not emit `Retry-After`,

    `X-RateLimit-*`, or any other backoff hint on a `429` — back off on your

    own schedule.


    ## Error responses


    **Six different error body shapes are in use across this service and they

    are not interchangeable.** Check the shape documented on the specific

    operation before writing a parser; a client that assumes one shape will

    read `undefined` for the reason on the others.


    1. `OrderErrorResponse` — the structured envelope (`error`, `code`,
       `error_details{code,message,details?,metadata?,actions}`) returned by
       every handler that surfaces an `ExecutionError`/`ApiError`: order
       submission, cancel-all, the CTF endpoints, the Polymarket/Opinion
       onboarding endpoints, Hyperliquid withdraw/transfer, and most of the
       deposit-wallet family. Note the two `code` fields differ in case:
       top-level `code` is PascalCase (`"AuthInsufficientScope"`), while
       `error_details.code` is the SCREAMING_SNAKE_CASE wire code
       (`"AUTH_INSUFFICIENT_SCOPE"`). Match on `error_details.code`.
    2. `OrderSimpleErrorResponse` — the minimal `{"error": "..."}` (sometimes
       with `code`) used by the auth middleware (any endpoint's
       `401`/`403`/`429`), the whole `/v2/*` external-signing and on-chain
       lane, the Kalshi and Predict.fun endpoints.
    3. `OrderMarketLinkErrorResponse` — `{"message": "..."}`, with no `error`
       and no `code`. Used by `POST /orders/market-links` **only**.
    4. **Empty or plain-text, not JSON at all.** Several deposit-wallet
       endpoints — most importantly
       the RPC signed-batch submission and
       `POST /exchanges/polymarket/imported/relay-info` — return most 4xx/5xx
       responses with a **completely empty body** and `content-type:
       text/plain`. A handful of cases carry a bare plain-text sentence
       (`batch is not an allowed withdrawal or collateral conversion`,
       `relayer rejected batch: …`). Do not attempt to JSON-parse these.
    5. Handlers whose Rust signature returns a bare `StatusCode` likewise send
       **no body at all**; those responses are marked "empty body (status code
       only)".
    6. `SyntheticBookErrorResponse` — `{"error": "stable_snake_case_code",
       "message": "...", "details"?: {...}}` on `/v1/synthetics*`. Match the
       top-level `error`; upstream definition validation may be nested under
       `details`.

    A further wrinkle inside shape 1: some provider-access checks discard the

    specific reason and return a generic `"Request failed with status 403"` /

    `AUTH_CREDENTIALS_INVALID` body, while others preserve

    `AUTH_INSUFFICIENT_SCOPE` and the real message. Do not rely on the message

    text of an access denial being stable.


    The central `ExecutionError` → HTTP mapping (`ApiError::from`) is:


    | `ExecutionError` | Status | `error_details.code` |

    |---|---|---|

    | `InsufficientBalance` | 400 | `FUNDS_INSUFFICIENT_USDC` |

    | `CollateralLocation` | 400 | `FUNDS_COLLATERAL_LOCATION` |

    | `InvalidOrder` | 400 | `VALIDATION_INVALID_ORDER` |

    | `PositionShortfall` | 400 | `FUNDS_INSUFFICIENT_BALANCE` |

    | `MarketClosed` | 400 | `EXCHANGE_POLYMARKET_MARKET_CLOSED` |

    | `OrderAlreadyCancelled` | 400 | `VALIDATION_INVALID_ORDER` |

    | `UnsupportedExchange` | 400 | `EXCHANGE_UNSUPPORTED` |

    | `SlippageExceeded` | 400 | `MARKET_FOK_NOT_FILLED` |

    | `FokNotFilled` | 400 | `MARKET_FOK_NOT_FILLED` |

    | `AuthenticationError` | 401 | `AUTH_CREDENTIALS_INVALID` |

    | `CredentialError` | 401 | `AUTH_CREDENTIALS_NOT_FOUND` |

    | `MarketNotFound` | 404 | `VALIDATION_MARKET_NOT_FOUND` |

    | `OrderNotFound` | 404 | `VALIDATION_INVALID_ORDER` |

    | `MarketNotSettledOnChain` | 409 | `VALIDATION_MARKET_NOT_SETTLED_ON_CHAIN`
    |

    | `OrderbookUnavailable` | 422 | `ORDERBOOK_UNAVAILABLE` |

    | `ExchangeError` (code `429`/`RATE_LIMITED`) | 429 |
    `EXCHANGE_POLYMARKET_RATE_LIMITED` |

    | `ExchangeError` (code `401`/`UNAUTHORIZED`) | 401 |
    `AUTH_POLYMARKET_API_KEY_INVALID` |

    | `SigningError` | 500 | `SIGNATURE_ERROR` |

    | `DatabaseError` | 500 | `DATABASE_ERROR` |

    | `InternalError` / `LedgerReconciliationRequired` /
    `SponsoredRequestWedged` / `PreTradeError` | 500 | `INTERNAL_ERROR` |

    | `NetworkError`, `ExchangeError` (any other code) | 502 | `NETWORK_ERROR` /
    venue-classified |

    | `LockError` | 503 | `INTERNAL_ERROR` |

    | `Timeout` | 504 | `NETWORK_TIMEOUT` |


    A venue `ExchangeError` is further classified from the venue's own message

    text before it is mapped, so an "allowance is not enough" rejection becomes

    `ALLOWANCE_CTF_NOT_SET`, a "not enough balance" rejection becomes

    `FUNDS_INSUFFICIENT_BALANCE`, "post-only mode" becomes `MARKET_NOT_READY`,

    a "no liquidity" rejection becomes `MARKET_INSUFFICIENT_LIQUIDITY`, and so

    on. Match on `error_details.code`, never on `error`.


    ## Server-level guards


    Every request is subject to a 120 s timeout (`OE_REQUEST_TIMEOUT_SECS`), a

    2 MiB request-body cap (`OE_MAX_BODY_BYTES`, over-size bodies get `413`),

    and a 1024-request global concurrency ceiling

    (`OE_MAX_CONCURRENT_REQUESTS`). Every response carries
    `X-Content-Type-Options:

    nosniff`, `X-Frame-Options: DENY`, HSTS, and a `default-src 'none'` CSP.


    ## Conventions


    - Prices are decimal strings on the 0–1 scale; `*_bps` fields are the
      same value in basis points (× 10000).
    - Quantities/sizes are decimal strings.

    - Structured errors carry `error_details.code`
      (SCREAMING_SNAKE_CASE) plus actionable recovery `actions`.

    ## Changelog


    **2026-09-14 — collateral routing fields on `POST /orders`.** Four optional

    request fields and one optional response field. `collateral`

    (`skip` | `check` | `fund`) **defaults to** `skip`, which is exactly today's

    path: no affordability check, no hold, no funding, zero added latency — so

    no existing caller changes behaviour without opting in. `shard_funding`

    **defaults to** `true`, which is also exactly today's behaviour: the Kalshi

    shard move is same-venue, zero-fee and already runs for every account; send

    `false` to opt out. `collateral=fund` requires both `max_bridge_fee_usdc`

    and `max_funding_wait_ms` and is rejected `400` naming the missing cap;

    either cap without `fund` is likewise a `400`. The response gains

    `funding`, `null` whenever no funding work ran. FIX sessions carry the same

    fields on `NewOrderSingle(D)` as optional tags `5701`–`5704`; a session that

    sends none of them is unchanged.
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/api-reference
  termsOfService: https://kairos.trade/terms
servers:
  - url: https://execution.kairos.trade
    description: Production (central primary, us-east-1)
  - url: https://eu-west-1-polymarket.executor.kairos.trade
    description: >-
      Production regional execution node — Ireland, colocated with Polymarket.
      Same API surface; assigned at onboarding.
  - url: https://ap-northeast-1-predictfun.executor.kairos.trade
    description: >-
      Production regional execution node — Tokyo, colocated with Predict.fun.
      Same API surface; assigned at onboarding.
  - url: https://staging-execution.kairos.trade
    description: Staging
security:
  - apiKeyClientId: []
    apiKeyKey: []
    apiKeySecret: []
paths:
  /orders:
    post:
      tags:
        - Orders
      summary: Submit a new order
      description: >-
        Submits a custodial order: Kairos signs and routes it to the venue on
        the caller's behalf (custodial signing). This is the standard
        order-entry path used by both the web app and API-key consumers who have
        not been allow-listed onto the self-custody "external-signing" lane
        (`POST /v2/orders/intent` + `POST /v2/orders/submit`).

        **Ownership.** The order is always created for the authenticated caller;
        any `user_id` in the body is accepted for backwards compatibility but is
        ignored — the JWT/API-key identity wins.

        **Ack semantics (async).** A 200 response means the order was validated,
        persisted, and enqueued — it does NOT mean the order is live on the
        venue yet. The response `status` is `"queued"` (or, on an idempotent
        replay, the current status of the previously-created order). Track the
        order via `GET /orders/{order_id}`, `GET /orders` polling, or (lowest
        latency) the `/ws` socket, which pushes `order_update` / `fill` events
        as the worker submits to the venue and fills arrive.

        **Idempotency.** Pass `client_order_id` to make retries safe: a second
        submit with the same `(user, exchange_id, market_id, client_order_id)`
        tuple returns the existing order instead of creating a duplicate, and
        does **not** consume a rate-limit slot. If omitted, the server generates
        a random one (i.e. retries without a client-supplied id are NOT
        deduped).

        **Provider routing.** `exchange_id` selects the venue-specific executor
        (`polymarket`, `kalshi`, `predictfun`, `opinion`, `hyperliquid`, …;
        `kalshi_offchain` is a deprecated alias of `kalshi`). Each venue
        advertises its own capabilities (supported `time_in_force` values, max
        price, order types) — a request that is well-formed in general but
        unsupported by the specific venue is rejected with `400
        EXCHANGE_UNSUPPORTED` / `400 VALIDATION_INVALID_ORDER`.

        **Hyperliquid HIP-4.** Use the numeric outcome id as `market_id`, the
        selected display label as `outcome`, and the side coin from the
        market-data `OrderbookSnapshot.token_ids` as `token_id` (for example
        `#1010` or `#1011`). Quantities are whole shares. Hyperliquid maps
        `GTC`/`GTD` to venue GTC and `IOC`/`FAK`/`FOK` to venue IOC.
        Single-order and selected-order batch cancellation are supported;
        cancel-all and fee quotes are unavailable.

        **Order types.** `kind="market"` is a marketable taker order — pair it
        with `time_in_force` `FOK` (fill-or-kill) or `FAK`/`IOC` (fill-and-kill
        / immediate-or-cancel, partials allowed, remainder cancelled).
        `kind="limit"` is a resting quote — pair it with `GTC`
        (good-til-cancelled) or `GTD` (good-til-date, requires
        `expiration_minutes`). `price` is REQUIRED for every order, including
        market orders — for a marketable order it is the limit you are willing
        to cross to, not a market-price sentinel.

        **Circuit breakers & gating.** Before persisting, the order passes
        through, in order: the per-user order rate limit, size/price bounds, the
        venue time-in-force capability gate, a global + per-exchange execution
        kill-switch, the custodial trading-enabled check, the post-only
        capability gate, side/kind/TIF-specific breakers, and the API-key
        minimum-notional rule. Kill-switch and restriction rejections are `503`
        with `error_details.code = MARKET_PAUSED`; a disabled or identity-less
        account is `403` with `AUTH_CREDENTIALS_INVALID`. Designated canary
        users bypass both breaker checks.

        **Rate limiting.** A Redis sliding-window limiter caps order submissions
        per authenticated user (default 5 orders per second,
        `ORDER_RATE_LIMIT_PER_SEC`). API keys with an `orders` override are
        checked against BOTH their own per-credential window and the aggregate
        per-user window (either denying is a `429`); idempotent replays are
        resolved before the limiter and never consume a slot. The limiter fails
        closed — an unreachable Redis denies with `429`. The `429` body carries
        `error_details.code = VALIDATION_INVALID_ORDER` with the message `Order
        rate limit exceeded` (there is no dedicated rate-limit code on this
        path), and no `Retry-After` header.

        **Auth & scope.** Requires `trade:execute`. This mutation endpoint is
        additionally gated by a service-token/CSRF/API-key check
        (`RequireServiceToken`), which any request bearing `X-Client-Id` /
        `X-Api-Key` / `X-Api-Secret` satisfies automatically — no extra header
        is needed for API-key consumers.
      operationId: submitOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderSubmitRequest'
            examples:
              hyperliquid:
                summary: Hyperliquid HIP-4 limit buy on side 1
                value:
                  exchange_id: hyperliquid
                  market_id: '101'
                  token_id: '#1011'
                  outcome: 'No'
                  side: buy
                  kind: limit
                  quantity: 25
                  price: 0.42
                  time_in_force: GTC
      responses:
        '200':
          description: >-
            Order accepted, persisted, and enqueued for execution. Does not
            imply the order is live on the venue yet — poll or subscribe to
            `/ws` for the terminal state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSubmitResponse'
        '400':
          description: >-
            Validation or admission failure. `error_details.code` distinguishes
            them:

            - `VALIDATION_INVALID_ORDER` — unparseable `side`/`kind`,
            `time_in_force` present but unrecognized (never silently downgraded
            to `GTC`), `expiration_minutes` outside `[1, 43200]` for a GTD
            order, `max_slippage` outside `[0, 0.5]`, `max_slippage_cents`
            outside `[1, 99]`, `max_retries` > 20, a TIF or `post_only`
            combination the venue's capabilities do not advertise, an
            unrecognized `collateral` mode, `collateral=fund` without
            `max_bridge_fee_usdc` or `max_funding_wait_ms` (the message names
            the missing cap), either cap sent with a mode other than `fund`, an
            API-key BUY under the `$5` minimum notional.

            - `VALIDATION_INVALID_SIZE` — quantity not positive, below
            `MIN_ORDER_QUANTITY` (BUY only), or above `1,000,000`.

            - `VALIDATION_INVALID_PRICE` — `price` missing (it is required for
            EVERY order, including market orders), not positive, or above the
            venue's max price; same bounds for `trigger_price`.

            - `EXCHANGE_UNSUPPORTED` — `exchange_id` is not a registered
            exchange.

            - `FUNDS_INSUFFICIENT_USDC` / `FUNDS_INSUFFICIENT_BALANCE` — a
            pre-trade balance check or a venue balance rejection. NOTE: an
            insufficient-balance reject is a `400`, not a `502`.

            - `MARKET_FOK_NOT_FILLED` — a FOK/marketable order could not be
            filled, or realized slippage exceeded `max_slippage_cents`.

            - `EXCHANGE_POLYMARKET_MARKET_CLOSED` — the market is closed /
            trading halted.

            - `ALLOWANCE_CTF_NOT_SET` / `ALLOWANCE_USDC_NOT_SET` — a missing
            on-chain approval, classified from the venue's rejection text.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: >-
            Missing `trade:execute` scope (`API key missing trade:execute
            scope`), API-key access to this provider disabled (`API-key access
            to <provider> is disabled`), or trading not enabled / no Turnkey
            identity for the user's custodial signing identity
            (`AUTH_CREDENTIALS_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '404':
          description: >-
            `VALIDATION_MARKET_NOT_FOUND` — the market/contract could not be
            resolved on the venue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '422':
          description: >-
            `ORDERBOOK_UNAVAILABLE` — a market order could not be priced because
            the live orderbook is missing or went stale between pricing and
            submission. Retryable by the caller (a fresh book may arrive); never
            auto-retried server-side within the same attempt.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '429':
          description: >-
            Per-user (and, for keys with an `orders` override, per-credential)
            order-submission rate limit exceeded, or the limiter's Redis was
            unreachable (fails closed). Body carries `error_details.code =
            VALIDATION_INVALID_ORDER`, message `Order rate limit exceeded`. A
            venue-side rate limit instead surfaces as
            `EXCHANGE_POLYMARKET_RATE_LIMITED`. No `Retry-After` header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '500':
          description: >-
            `INTERNAL_ERROR` (capabilities unavailable, trading-status read
            failed, idempotency service unavailable, pre-trade preparation
            failed), `DATABASE_ERROR`, or `SIGNATURE_ERROR` (custodial signing
            failed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '502':
          description: >-
            The venue call failed — a network/RPC error (`NETWORK_ERROR`) or an
            unclassified exchange rejection (`EXCHANGE_ERROR`). Balance,
            allowance and market-closed rejections are classified out of this
            bucket into `400`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '503':
          description: >-
            Execution disabled by a circuit breaker (global or per-exchange
            halt, side/kind/TIF restriction) — `MARKET_PAUSED`; or an execution
            lock could not be acquired (`INTERNAL_ERROR`, "System is busy").
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
        '504':
          description: >-
            `NETWORK_TIMEOUT` — the venue call timed out. The order may still
            have reached the venue; verify with `GET /orders/{order_id}` before
            resubmitting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderErrorResponse'
components:
  schemas:
    OrderSubmitRequest:
      type: object
      description: Body for `POST /orders` (custodial order submission).
      required:
        - exchange_id
        - market_id
        - side
        - kind
        - quantity
      properties:
        user_id:
          type: string
          deprecated: true
          description: Ignored. The authenticated caller's id is always used.
        exchange_id:
          type: string
          description: >-
            Venue identifier, must be a registered exchange (e.g. `polymarket`,
            `kalshi`, `predictfun`, `hyperliquid`).
          example: polymarket
        market_id:
          type: string
          description: >-
            Market/contract identifier on the venue. Hyperliquid expects the
            numeric HIP-4 outcome id.
        token_id:
          type: string
          nullable: true
          description: >-
            Outcome-token id. For Hyperliquid, pass the selected side coin `#<10
            * market_id + side_index>`; it is required to select side 1.
        outcome:
          type: string
          nullable: true
          description: >-
            Human-readable outcome label for display/attribution (e.g. `"Yes"`,
            `"No"`, a team name).
          example: 'Yes'
        side:
          type: string
          description: '`buy` or `sell`, case-insensitive on input.'
          enum:
            - buy
            - sell
            - BUY
            - SELL
            - Buy
            - Sell
          example: buy
        kind:
          type: string
          description: '`market` or `limit`, lower-case exactly.'
          enum:
            - market
            - limit
          example: limit
        quantity:
          type: string
          description: >-
            Decimal string, shares/contracts. Must be > 0 and <= 1,000,000. A
            minimum order size (default `1.0`, `MIN_ORDER_QUANTITY`) applies to
            BUY orders only — SELL/close orders are allowed at any size so a
            position can be fully closed after partial fills.
            API-key-authenticated BUYs additionally require `quantity × price >=
            $5` notional; session-JWT callers and all SELLs are exempt.
          example: '100'
        price:
          type: string
          description: >-
            Decimal string, REQUIRED for every order (including market orders,
            where it is the limit you're willing to cross to). Must be > 0 and
            <= the venue's max price (typically `1.0` for prediction markets).
          example: '0.52'
        time_in_force:
          type: string
          description: >-
            `GTC`, `GTD`, `FOK`, `FAK`, or `IOC` (case-insensitive on input,
            normalized to upper-case). Defaults to `GTC` if omitted; an
            unrecognized non-empty value is rejected rather than silently
            downgraded to `GTC`. Must be a TIF the target venue's capabilities
            advertise.
          default: GTC
          example: GTC
        post_only:
          type: boolean
          nullable: true
          description: >-
            Maker-only. When `true` the venue must REJECT the order rather than
            let any part of it cross the spread and take liquidity — it is a
            guarantee, never a hint, so an order that cannot honour it is
            rejected instead of being downgraded to a taker. Requires
            `kind=limit` and `time_in_force` of `GTC` or `GTD`, on a venue whose
            capabilities advertise post-only support (currently Polymarket,
            Kalshi, Predict.fun and Hyperliquid — the last expresses maker-only
            as its `Alo` time-in-force rather than a flag, but the request field
            is the same). Any other combination is a 400. Defaults to `false`.
          default: false
          example: false
        expiration_minutes:
          type: integer
          nullable: true
          description: >-
            Minutes from now until expiry. Only meaningful (and validated) when
            `time_in_force=GTD`: must be in `[1, 43200]` (30 days).
          example: 60
        trigger_price:
          type: string
          nullable: true
          description: >-
            Decimal string. Trigger price for stop-loss/take-profit style
            orders. Must be > 0 and, like `price`, <= the venue's max price.
        collateral:
          type: string
          nullable: true
          enum:
            - skip
            - check
            - fund
          default: skip
          description: >-
            How much of the collateral router runs for this order. `skip` (the
            server default) is today's path: no affordability check, no hold, no
            router call, zero added latency. `check` places an affordability
            check and a hold and refuses a known shortfall, but never moves
            money. `fund` is `check` plus cross-ledger funding inside the two
            caps below, and REQUIRES both of them. An unrecognised value is
            rejected `400` naming the three valid values; it is never downgraded
            to `skip`.
          example: skip
        shard_funding:
          type: boolean
          nullable: true
          default: true
          description: >-
            Kalshi only. Move collateral between the account's own Kalshi shards
            before submit. Defaults to `true` — same venue, zero fee, one REST
            call, and already every account's behaviour — so an existing caller
            is unaffected. Send `false` to skip it.
          example: true
        max_bridge_fee_usdc:
          type: string
          nullable: true
          description: >-
            Decimal string. The most bridge fee this order consents to pay, in
            USDC. Required when `collateral=fund` and rejected `400` otherwise;
            `0` is a valid cap meaning "fund only if it is free".
          example: '1.50'
        max_funding_wait_ms:
          type: integer
          nullable: true
          description: >-
            The longest this order consents to wait for funding, in
            milliseconds. Required when `collateral=fund` and rejected `400`
            otherwise.
          example: 30000
        gas_sponsored:
          type: boolean
          nullable: true
          description: Requests gas sponsorship. Currently not implemented server-side.
        client_order_id:
          type: string
          nullable: true
          description: >-
            Caller-supplied idempotency key. A retry with the same `(user,
            exchange_id, market_id, client_order_id)` returns the existing order
            instead of creating a duplicate, and does not consume a rate-limit
            slot. If omitted, a random id is generated server-side (so un-keyed
            retries are NOT deduped).
        orderbook_levels:
          type: array
          deprecated: true
          description: >-
            Ignored. The executor always fetches fresh orderbook data itself.
            Kept only for backwards-compatible request bodies.
          items:
            type: array
            items:
              type: string
        max_slippage:
          type: string
          nullable: true
          deprecated: true
          description: >-
            Deprecated — use `max_slippage_cents`. Decimal in `[0, 0.5]` (e.g.
            `0.10` = 10%).
        max_slippage_cents:
          type: integer
          nullable: true
          description: >-
            Maximum acceptable slippage in cents. Must be in `[1, 99]` if
            provided.
          example: 5
        max_retries:
          type: integer
          nullable: true
          description: Maximum retry attempts for transient failures. Defaults to `0`.
        bot_id:
          type: string
          nullable: true
          description: Trading-bot id this order is attributed to.
        source:
          type: string
          nullable: true
          description: >-
            Order-source attribution override. Pass `copytrade` explicitly when
            relevant; otherwise the server derives it from the auth method
            (API-key auth → `api`, `bot_id` present → `bot`, else → `manual`).
          example: api
    OrderSubmitResponse:
      type: object
      description: Response for `POST /orders`.
      required:
        - order_id
        - status
      properties:
        order_id:
          type: string
          format: uuid
          description: >-
            Internal Kairos order id. Use with `GET /orders/{order_id}` and the
            cancel endpoints.
        status:
          type: string
          description: >-
            `queued` for a newly-created order; on an idempotent replay
            (matching `client_order_id`), the pre-existing order's current
            status (`pending`, `live`, `partial`, `filled`, `cancelled`,
            `expired`, or `failed`).
          example: queued
        funding:
          nullable: true
          description: >-
            What the collateral router did for this order. `null` whenever no
            funding work ran — every `skip` order, and every order while funding
            orchestration is still being built.
          allOf:
            - $ref: '#/components/schemas/FundingOutcome'
    OrderErrorResponse:
      type: object
      description: >-
        Standard structured error body returned by every endpoint that surfaces
        an `ExecutionError`/`ApiError` (order submission, cancel-all, and the
        other endpoints noted as returning this schema). Endpoints noted as
        "empty body (status code only)" do NOT use this shape.
      required:
        - error
        - error_details
      properties:
        error:
          type: string
          description: >-
            Same text as `error_details.message`, kept for backwards-compatible
            consumers.
        code:
          type: string
          nullable: true
          description: >-
            Debug-formatted copy of `error_details.code` (e.g.
            `"FundsInsufficientUsdc"` — PascalCase, NOT the SCREAMING_SNAKE_CASE
            wire code in `error_details.code`).
        error_details:
          $ref: '#/components/schemas/OrderErrorDetails'
    FundingOutcome:
      type: object
      description: >-
        What the collateral router did for one order. Absent sub-fields mean
        that part did not happen.
      required:
        - mode
      properties:
        mode:
          type: string
          enum:
            - skip
            - check
            - fund
          description: The resolved collateral mode this outcome describes.
        hold_id:
          type: string
          format: uuid
          description: The BUY hold the affordability check placed, when one was placed.
        intent_ref:
          type: string
          description: The router intent that moved collateral, when one was opened.
        refused_reason:
          type: string
          description: Why funding did not happen, for a mode that asked for it.
    OrderErrorDetails:
      type: object
      description: >-
        Structured, machine-readable error payload shared across order-execution
        error responses.
      required:
        - code
        - message
        - actions
      properties:
        code:
          $ref: '#/components/schemas/OrderErrorCode'
        message:
          type: string
          description: Human-readable error message.
        details:
          type: string
          nullable: true
          description: Optional extended explanation.
        metadata:
          nullable: true
          description: >-
            Error-type-specific structured metadata (shape varies by `code` —
            e.g. `{type: "insufficient_funds", required, available, currency,
            shortfall}` or `{type: "rate_limit", retry_after_seconds, limit,
            window}`). Untagged union; treat as opaque unless you match on the
            embedded `type` field.
          additionalProperties: true
        actions:
          type: array
          description: Zero or more actionable recovery steps a client can surface.
          items:
            $ref: '#/components/schemas/OrderErrorAction'
    OrderErrorCode:
      type: string
      description: Machine-readable error code, SCREAMING_SNAKE_CASE on the wire.
      enum:
        - AUTH_CREDENTIALS_NOT_FOUND
        - AUTH_CREDENTIALS_INVALID
        - AUTH_POLICY_OUTDATED
        - AUTH_INSUFFICIENT_SCOPE
        - AUTH_POLYMARKET_API_KEY_INVALID
        - AUTH_TURNKEY_WALLET_NOT_FOUND
        - AUTH_TURNKEY_AUTH_FAILED
        - AUTH_TURNKEY_SIGNING_FAILED
        - VALIDATION_INVALID_ORDER
        - VALIDATION_INVALID_PRICE
        - VALIDATION_INVALID_SIZE
        - VALIDATION_MARKET_NOT_FOUND
        - VALIDATION_TOKEN_NOT_FOUND
        - VALIDATION_ORDER_EXPIRED
        - VALIDATION_MARKET_NOT_SETTLED_ON_CHAIN
        - AUTH_IDENTITY_MISMATCH
        - FUNDS_INSUFFICIENT_USDC
        - FUNDS_INSUFFICIENT_GAS
        - FUNDS_INSUFFICIENT_TOTAL
        - FUNDS_INSUFFICIENT_BALANCE
        - FUNDS_COLLATERAL_LOCATION
        - ALLOWANCE_USDC_NOT_SET
        - ALLOWANCE_CTF_NOT_SET
        - ALLOWANCE_APPROVAL_FAILED
        - NETWORK_RPC_ERROR
        - NETWORK_TIMEOUT
        - NETWORK_HTTP_ERROR
        - NETWORK_ERROR
        - EXCHANGE_POLYMARKET_API_ERROR
        - EXCHANGE_POLYMARKET_UNAUTHORIZED
        - EXCHANGE_POLYMARKET_MARKET_CLOSED
        - EXCHANGE_POLYMARKET_RATE_LIMITED
        - EXCHANGE_KALSHI_API_ERROR
        - EXCHANGE_KALSHI_UNAUTHORIZED
        - EXCHANGE_KALSHI_MARKET_CLOSED
        - EXCHANGE_ERROR
        - EXCHANGE_UNSUPPORTED
        - SIGNATURE_EIP712_FAILED
        - SIGNATURE_ERROR
        - GAS_ESTIMATION_FAILED
        - TRANSACTION_FAILED
        - GAS_SPONSORSHIP_FAILED
        - TRANSACTION_TIMEOUT
        - MARKET_NOT_READY
        - MARKET_PAUSED
        - MARKET_INSUFFICIENT_LIQUIDITY
        - ORDERBOOK_UNAVAILABLE
        - MARKET_FOK_NOT_FILLED
        - MARKET_ORDER_SIZE_EXCEEDS_MAX
        - DATABASE_ERROR
        - DATABASE_CREDENTIAL_DECRYPTION_FAILED
        - INTERNAL_ERROR
      example: FUNDS_INSUFFICIENT_USDC
    OrderErrorAction:
      type: object
      description: An actionable recovery step a client can surface to the end user.
      required:
        - action
        - label
        - primary
      properties:
        action:
          $ref: '#/components/schemas/OrderErrorActionType'
        label:
          type: string
          description: Human-readable button/link label.
          example: Add Funds
        url:
          type: string
          nullable: true
        primary:
          type: boolean
          description: Whether this is the primary/recommended action.
    OrderErrorActionType:
      type: string
      description: Machine-readable recovery-action identifier, snake_case on the wire.
      enum:
        - enable_trading
        - reenable_trading
        - update_policies
        - regenerate_api_key
        - contact_support
        - retry
        - review_order
        - adjust_price
        - adjust_size
        - browse_markets
        - refresh_market
        - create_new_order
        - add_funds
        - reduce_size
        - swap_usdc
        - add_matic
        - approve_usdc
        - approve_ctf
        - retry_approval
        - wait_and_retry
        - check_status
        - use_limit_order
        - update_kalshi_credentials
        - enable_bridge_funding
  responses:
    ExecUnauthorized:
      description: >
        No valid credential presented — missing/invalid API-key headers or an
        invalid/expired session token.
      content:
        application/json:
          schema:
            type: object
            required:
              - error
            properties:
              error:
                type: string
                example: Unauthorized
  securitySchemes:
    apiKeyClientId:
      type: apiKey
      in: header
      name: X-Client-Id
      description: >-
        Credential client id (`kairos_ck_...`). Must be sent together with
        X-Api-Key and X-Api-Secret.
    apiKeyKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: 64-char hex API key.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: 64-char hex API secret.

````

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