---
title: "Submit a new order"
method: POST
path: "/orders"
tags: ["Orders"]
---

# Submit a new order

`POST /orders`

## Request

- Base URL: `https://execution.kairos.trade`
- URL: `https://execution.kairos.trade/orders`
- Auth: API key in header `X-Client-Id` and API key in header `X-Api-Key` and API key in header `X-Api-Secret`

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.

## Request body

- OrderSubmitRequest: Body for `POST /orders` (custodial order submission).
  - `user_id` string: Ignored. The authenticated caller's id is always used.
  - `exchange_id` string, required: Venue identifier, must be a registered exchange (e.g. `polymarket`, `kalshi`, `predictfun`, `hyperliquid`).
  - `market_id` string, required: Market/contract identifier on the venue. Hyperliquid expects the numeric HIP-4 outcome id.
  - `token_id` string, nullable: Outcome-token id. For Hyperliquid, pass the selected side coin `#<10 * market_id + side_index>`; it is required to select side 1.
  - `outcome` string, nullable: Human-readable outcome label for display/attribution (e.g. `"Yes"`, `"No"`, a team name).
  - `side` 'buy' | 'sell' | 'BUY' | 'SELL' | 'Buy' | 'Sell', required: `buy` or `sell`, case-insensitive on input.
  - `kind` 'market' | 'limit', required: `market` or `limit`, lower-case exactly.
  - `quantity` string, required: 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.
  - `price` string: 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).
  - `time_in_force` string: `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.
  - `post_only` boolean, nullable: 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`.
  - `expiration_minutes` integer, nullable: Minutes from now until expiry. Only meaningful (and validated) when `time_in_force=GTD`: must be in `[1, 43200]` (30 days).
  - `trigger_price` string, nullable: Decimal string. Trigger price for stop-loss/take-profit style orders. Must be > 0 and, like `price`, <= the venue's max price.
  - `collateral` 'skip' | 'check' | 'fund', nullable: 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`.
  - `shard_funding` boolean, nullable: 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.
  - `max_bridge_fee_usdc` string, nullable: 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".
  - `max_funding_wait_ms` integer, nullable: The longest this order consents to wait for funding, in milliseconds. Required when `collateral=fund` and rejected `400` otherwise.
  - `gas_sponsored` boolean, nullable: Requests gas sponsorship. Currently not implemented server-side.
  - `client_order_id` string, nullable: 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). On `hyperliquid` it is sent unchanged as the order's cloid and must be `0x` followed by 32 lowercase hex characters; any other value is refused with `400`.
  - `orderbook_levels` array[]: Ignored. The executor always fetches fresh orderbook data itself. Kept only for backwards-compatible request bodies.
    - string[]
  - `max_slippage` string, nullable: Deprecated — use `max_slippage_cents`. Decimal in `[0, 0.5]` (e.g. `0.10` = 10%).
  - `max_slippage_cents` integer, nullable: Maximum acceptable slippage in cents. Must be in `[1, 99]` if provided.
  - `max_retries` integer, nullable: Maximum retry attempts for transient failures. Defaults to `0`.
  - `bot_id` string, nullable: Trading-bot id this order is attributed to.
  - `source` string, nullable: 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`).

## Response `200`

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.

- OrderSubmitResponse: Response for `POST /orders`.
  - `order_id` string, uuid, required: Internal Kairos order id. Use with `GET /orders/{order_id}` and the cancel endpoints.
  - `status` string, required: `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`).
  - `funding` FundingOutcome: What the collateral router did for one order. Absent sub-fields mean that part did not happen.
    - `mode` 'skip' | 'check' | 'fund', required: The resolved collateral mode this outcome describes.
    - `hold_id` string, uuid: The BUY hold the affordability check placed, when one was placed.
    - `intent_ref` string: The router intent that moved collateral, when one was opened.
    - `refused_reason` string: Why funding did not happen, for a mode that asked for it.

## Other responses

- `400`: 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.
- `401`: No valid credential presented — missing/invalid API-key headers or an invalid/expired session token.
- `403`: 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`).
- `404`: `VALIDATION_MARKET_NOT_FOUND` — the market/contract could not be resolved on the venue.
- `422`: `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.
- `429`: 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.
- `500`: `INTERNAL_ERROR` (capabilities unavailable, trading-status read failed, idempotency service unavailable, pre-trade preparation failed), `DATABASE_ERROR`, or `SIGNATURE_ERROR` (custodial signing failed).
- `502`: 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`.
- `503`: 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").
- `504`: `NETWORK_TIMEOUT` — the venue call timed out. The order may still have reached the venue; verify with `GET /orders/{order_id}` before resubmitting.

---

[API](https://skmtc.dev/kairos/apis/kairos-order-execution-api.md) - [All operations](https://skmtc.dev/kairos/apis/kairos-order-execution-api/llms.txt) - [OpenAPI document](https://skmtc.dev/kairos/apis/kairos-order-execution-api/revisions/9ba7d463d505?raw)
