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

# Orders

> Submit, query, and cancel orders on the custodial (Kairos-signed) order lane

<Note>
  **Prefer WebSocket for order submission.** Placing and cancelling orders over the persistent `/ws` socket is lower-latency (one round trip instead of the REST submit/poll pair) and simpler to build: one connection, one auth handshake, and pushed `order_update`/`fill` events instead of polling. See [Order Execution over WebSocket](/websocket/order-execution).
</Note>

Submit, cancel, and query orders on the custodial lane, where Kairos signs and routes on your behalf — see [External Execution](/external-execution/overview) for the self-custody signing lane. Use this page when you are placing orders from a bot or backend and need the exact request shape, status semantics, and error codes.

Full parameter reference and a live tester for every endpoint below: [API Reference](/api-reference).

## Base URL

```
https://execution.kairos.trade
```

## Authentication

Every endpoint on this page requires a Kairos API key or session JWT plus a per-operation scope (`trade:execute` or `trade:read`) — there is no anonymous tier on this service, it moves money.

```
X-Client-Id: kairos_ck_...
X-Api-Key: <64 hex chars>
X-Api-Secret: <64 hex chars>
```

Examples below read credentials from `KAIROS_CLIENT_ID`, `KAIROS_API_KEY`, and `KAIROS_API_SECRET` in your shell.

Order submission is rate-limited to 5/sec per user by default (sliding window); API keys with a per-key override are checked against both windows (the override window is per-minute). **Idempotent replays (matching `client_order_id`) never consume a slot** — the one exception is a Predict.fun request that passes an outcome token id as `market_id`, which is charged before the canonicalisation lookup.

The order-submitting and cancelling endpoints also sit behind a mutation gate that runs *before* user auth, so a bad API-key triple there is a **`403`**, not a `401`; the read endpoints (`GET /orders`, `GET /orders/{order_id}`, `GET /orders/fee-quote`) return `401`. Repeated auth failures from one source IP are throttled at 10/min and then return `429`.

## Submit Order

```
POST /orders
Scope: trade:execute
```

Validates, persists, and enqueues the order. Call it once per order you want on a venue; track the outcome via `GET /orders/{order_id}`, `GET /orders` polling, or (lowest latency) the `/ws` socket's `order_update` / `fill` events.

Auth: API key or session JWT, scope `trade:execute`. Rate limit: 5/sec per user by default.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `exchange_id` | string | Yes | — | A registered exchange: `polymarket`, `kalshi`, `predictfun`, `opinion`, `hyperliquid`, … (`kalshi_offchain` is a deprecated alias of `kalshi`, still accepted) |
| `market_id` | string | Yes | — | Market/contract identifier. For Polymarket: numeric Gamma market id, `0x` condition id, or a full CLOB token id. For Hyperliquid: numeric HIP-4 outcome id |
| `side` | string | Yes | — | `buy` or `sell`, case-insensitive |
| `kind` | string | Yes | — | `market` or `limit` |
| `quantity` | number | Yes | — | > 0, ≤ 1,000,000. Buy orders also subject to `MIN_ORDER_QUANTITY` (default `1.0`); sells have no minimum so a position can always be fully closed |
| `price` | number | Yes | — | > 0, ≤ venue max (typically `1.0`). Executable price in the selected outcome's own dollars — required even for market orders. Limit orders: the resting price. Market orders: the live same-outcome quote (ask for buys, bid for sells). For Kalshi `No`, send the displayed `No` price; do not complement it client-side |
| `outcome` | string | Required except Polymarket token-only requests | — | Outcome label (e.g. `"Yes"`, `"No"`). For Kalshi, only `"Yes"`/`"No"` are valid — display labels are not |
| `token_id` | string | Provider-specific | — | Polymarket: full CLOB id, >15 digits. Hyperliquid: `#<encoding>`. Outcome-token identifier; Hyperliquid uses it to select the side-specific book, see [Hyperliquid orders](#hyperliquid-orders) |
| `time_in_force` | string | No | `GTC` | `GTC`, `FOK`, `IOC`, `FAK`, or `GTD`, case-insensitive; empty/whitespace also means `GTC`. An unrecognised value is rejected `400`, never downgraded. See [Time-in-Force](#time-in-force) |
| `post_only` | boolean | No | `false` | Maker-only: the venue must reject the order rather than let it cross. Limit orders on `GTC`/`GTD` only, and only on venues advertising `supports_post_only` — anything else is rejected `400` rather than silently downgraded to a taker order |
| `expiration_minutes` | integer | GTD only | — | `1`–`43200` (30 days) |
| `client_order_id` | string | No | Auto-generated | Idempotency key — a retry with the same `(user, exchange_id, market_id, client_order_id)` returns the existing order instead of duplicating, and does not consume a rate-limit slot. Un-keyed retries are NOT deduped |
| `trigger_price` | number | No | — | Same bounds as `price`. Stop/take-profit trigger |
| `max_slippage_cents` | integer | No | — | `1`–`99` |
| `max_slippage` | number | No | — | `[0, 0.5]`. **Deprecated** — use `max_slippage_cents` |
| `max_retries` | integer | No | `0` | `0`–`20`. Retry attempts for transient failures; a value above `20` is rejected `400` |
| `collateral` | string | No | `skip` | How much of the collateral router runs for this order: `skip` (today's path — no affordability check, no hold, no funding, zero added latency), `check` (affordability check plus a hold; refuses a known shortfall, never moves money), `fund` (check plus cross-ledger funding inside the caps below). An unrecognised value is rejected `400` naming the three, never downgraded to `skip` |
| `shard_funding` | boolean | No | `true` | **Kalshi only.** Move collateral between your own Kalshi shards before submit. Defaults **on** — it is same-venue, zero-fee and already every account's behaviour. Send `false` to opt out |
| `max_bridge_fee_usdc` | number\|string | `fund` only | — | The most bridge fee this order consents to pay, in USDC. Non-negative, at most **8 decimal places**, and below `10000000000` — the exact range `numeric(18,8)` stores. `0` is a valid cap meaning "fund only if it is free". Anything outside that is rejected `400` |
| `max_funding_wait_ms` | integer | `fund` only | — | The longest this order consents to wait for funding, in milliseconds. `0`–`600000` (ten minutes); above that is rejected `400`. The **FIX** fields `KairosMaxBridgeFeeUSDC(5703)` / `KairosMaxFundingWaitMs(5704)` accept exactly the same grammar and the same bounds |
| `bot_id` | string | No | — | Copy-trading / strategy attribution |
| `source` | string | No | From auth method | `manual`, `bot`, `copytrade`, or `api`. Defaults from auth method when omitted (API key → `api`, `bot_id` present → `bot`, else `manual`) |
| `gas_sponsored` | boolean | No | — | **Not implemented server-side yet** — accepted but currently has no effect |
| `user_id` | string | No | — | **Ignored** — the authenticated caller is always used |
| `orderbook_levels` | array | No | — | **Ignored / deprecated** — the executor always fetches a fresh book |

<Note>
  **Gotcha:** `collateral: "fund"` requires **both** `max_bridge_fee_usdc` and `max_funding_wait_ms`. A `fund` order missing either is rejected `400` naming the missing cap — consent to fund is not consent to any fee. Symmetrically, sending either cap without `collateral: "fund"` is rejected `400` (`max_bridge_fee_usdc is only valid with collateral=fund, not collateral=skip`): a cap the mode can never spend is a mistake, not a hint.
</Note>

<Note>
  **Gotcha:** API-key BUYs have a $5 minimum notional. `quantity × price` below `$5`is rejected`400` (`VALIDATION\_INVALID\_ORDER\`). The rule applies only to BUYs authenticated with an API key — session-JWT callers and all SELLs are exempt.
</Note>

```bash theme={null}
curl -X POST https://execution.kairos.trade/orders \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "exchange_id": "polymarket",
    "market_id": "570362",
    "outcome": "Yes",
    "side": "buy",
    "kind": "limit",
    "quantity": 100,
    "price": 0.45,
    "time_in_force": "GTC"
  }'
```

### Response

```json theme={null}
{
  "order_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued",
  "funding": null
}
```

On an idempotent hit (existing `client_order_id`), `status` is the existing order's current status instead of `"queued"`.

`funding` reports what the collateral router did, and is `null` whenever no funding work ran — which is every `skip` order, and every order today while funding orchestration is still being built. When present it carries `{ "mode": "check" | "fund", "hold_id"?, "intent_ref"?, "refused_reason"? }`; absent sub-fields mean that part did not happen.

<Note>
  **Gotcha:** `queued` is only Kairos's acceptance acknowledgement — an async ack, not confirmation the order is live on the venue, accepted, or filled. Predict.fun in particular confirms settlement asynchronously on BSC; poll `GET /orders/{order_id}` or consume order updates for the authoritative result.
</Note>

### Time-in-Force

| Value | Name | Description |
| - | - | - |
| `GTC` | Good-til-Cancelled | Resting; stays active until filled or cancelled (default) |
| `GTD` | Good-til-Date | Resting; expires after `expiration_minutes` |
| `FOK` | Fill-or-Kill | Immediate taker; fill entirely or cancel |
| `IOC` | Immediate-or-Cancel | Immediate taker; fill what's available, cancel the rest |
| `FAK` | Fill-and-Kill | Same as IOC (Polymarket-specific alias) |

Predict.fun maps `FOK` to `isFillOrKill=true` and maps `FAK`/`IOC` to
`isFillOrKill=false`. `GTC` and `GTD` omit that immediate-fill flag and rest
until their signed expiration. A Predict.fun `GTC` limit receives a 30-day
signed expiration, while a `GTD` limit uses `expiration_minutes`; market orders
always use the venue's fixed five-minute expiration.

### Hyperliquid orders

HIP-4 has a separate book for each binary side. Use the numeric outcome id as
`market_id`, include the selected display label in `outcome`, and pass the side
coin from `OrderbookSnapshot.token_ids` as `token_id` (for example `#1010` or
`#1011`). If deriving it, `encoding = 10 * market_id + side_index`, where the
first outcome is side `0` and the second is side `1`. A bare `market_id` falls
back to side `0`, so `token_id` is required to trade side `1`.

Quantities are whole shares; fractional values are floored and values below one
share are rejected. 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.

Trading also requires a previously authorized Hyperliquid agent and sufficient
spot USDC. Bridge deposits arrive in the perp balance and must be moved to spot
before they can fund HIP-4 orders; see [Portfolio balances](/rpc/portfolio).

```json theme={null}
{
  "exchange_id": "hyperliquid",
  "market_id": "101",
  "token_id": "#1011",
  "outcome": "No",
  "side": "buy",
  "kind": "limit",
  "quantity": 25,
  "price": 0.42,
  "time_in_force": "GTC"
}
```

## Get Order

```
GET /orders/{order_id}
Scope: trade:read
```

Returns the full order, including `raw` (the unredacted venue response) and `metadata`. Call it to resolve the authoritative state of an order you submitted.

Auth: API key or session JWT, scope `trade:read`. You can only view your own orders.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `order_id` (path) | uuid | Yes | — | A Kairos order UUID. A non-UUID value is rejected `400` by the path parser before the handler runs |

```bash theme={null}
curl "https://execution.kairos.trade/orders/550e8400-e29b-41d4-a716-446655440000" \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET"
```

### Response

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "user_id": "...",
  "exchange_id": "polymarket",
  "market_id": "570362",
  "token_id": "12345678901234567890",
  "outcome": "Yes",
  "outcome_id": "12345678901234567890",
  "side": "buy",
  "kind": "limit",
  "quantity": "100",
  "price": "0.45",
  "price_bps": 4500,
  "time_in_force": "GTC",
  "post_only": false,
  "expires_at": null,
  "status": "live",
  "filled_quantity": "0",
  "avg_fill_price": null,
  "avg_fill_price_bps": null,
  "exchange_order_id": "0xabc...",
  "client_order_id": null,
  "wallet_id": "...",
  "trigger_price": null,
  "trigger_price_bps": null,
  "gas_sponsored": true,
  "collateral_mode": "skip",
  "shard_funding": true,
  "max_bridge_fee_usdc": null,
  "max_funding_wait_ms": null,
  "gas_amount": null,
  "maker_address": null,
  "tx_hash": null,
  "fee_amount": null,
  "fee_currency": null,
  "bot_id": null,
  "source": "api",
  "metadata": {},
  "holding_wallet": "",
  "error_message": null,
  "created_at": "2026-02-06T12:00:00Z",
  "updated_at": "2026-02-06T12:00:01Z"
}
```

| Field | Type | Description |
| - | - | - |
| `status` | string | See [Order Status Types](#order-status-types) |
| `price` / `avg_fill_price` | string | Decimal, 0–1 scale. `price_bps` / `avg_fill_price_bps` carry the same value ×10000 |
| `raw` | object\|null | Full venue request/response, for audit. Present here; stripped on `GET /orders` |
| `holding_wallet` | string | On-chain wallet holding the resulting shares (Safe proxy for upgraded/external-signing users, EOA otherwise). Empty string for non-Polymarket orders |
| `source` | string | `manual`, `bot`, `copytrade`, `api`, or `fastlane` (external-signing lane) |
| `post_only` | boolean | Maker-only guarantee as submitted |
| `collateral_mode` / `shard_funding` | string / boolean | The collateral decision resolved at submit. Always present — `skip` and `true` for an order that asked for nothing |
| `max_bridge_fee_usdc` / `max_funding_wait_ms` | string\|null / integer\|null | The order's funding caps. Only ever set under `collateral_mode: "fund"` |
| `failure` | object\|null | Structured failure detail; **omitted entirely** when the order has not failed. `error_message` stays populated for existing consumers. See [Order failures](#order-failures) |

### Order failures

When `status` is `failed`, `failure` carries `{ code, classification, details }`:

* `classification` is the **retry policy** and is authoritative: `expected_user_rejection` (the venue or your own inputs rejected a well-formed order — do not retry unchanged), `retryable` (transient; a retry may succeed), `non_retryable` (retrying the same order cannot succeed).
* `details.actions` is a **UI affordance only**. It is derived from `code` independently of `classification` and may include `retry` — even as the primary action — on a `non_retryable` failure. Do not build an automatic retry loop from `actions`; branch on `classification`.
* `code` is the execution error's wire code (e.g. `FOK_NOT_FILLED`); `details.code` is the `OrderErrorCode` used for grouping (e.g. `MARKET_FOK_NOT_FILLED`). For a venue `ExchangeError` the two coincide.

## List Orders

```
GET /orders
Scope: trade:read
```

Returns the caller's orders, newest first (`submittedAt DESC, id DESC`), so `limit` + `offset` paginate deterministically. For deep or long-lived paging prefer the `before_id` cursor, which cannot skip rows when the set shifts underneath you.

Auth: API key or session JWT, scope `trade:read`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `status` | string | No | — | A comma-separated set: a single status (`"filled"`) or several (`"live,partial"`). Matched literally against the stored value, so use the wire values from [Order Status Types](#order-status-types). Omit for all statuses |
| `exchange_id` | string | No | — | Filter to one venue |
| `active_only` | boolean | No | `false` | Only genuinely working orders. Unlike a status filter it also excludes a `partial` market/`FAK`/`IOC`/`FOK` order whose remainder was killed at submit |
| `limit` | integer | No | `50` | Max 500. Values above 500 are clamped, not rejected |
| `offset` | integer | No | `0` | ≥ 0. Page *p* (0-indexed) of size *n* is `limit=n&offset=p*n`. **Ignored when `before_id` is set** |
| `before_id` | uuid | No | — | Must be one of your own orders. Keyset cursor: return rows strictly older than that order on the stable `(submittedAt, id)` key. Unlike `offset` it cannot skip rows when the active set shifts between pages |
| `user_id` | string | No | — | **Ignored** — the authenticated caller is always used |

```bash theme={null}
# Most recent live + partially-filled Polymarket orders
curl "https://execution.kairos.trade/orders?status=live,partial&exchange_id=polymarket" \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET"
```

### Response

Order rows in the same shape as [Get Order](#get-order), newest first.

> **Gotcha:** this is a lighter payload than Get Order. `raw` and `metadata` are stripped to `null` on every row to keep this cheap to poll (`raw` alone can be \~100 KB/order) — the `outcome` label is preserved by falling back to `metadata.outcome` before stripping. Fetch a single order via `GET /orders/{order_id}` for the full row.

> Need current position exposure rather than order state? `GET /positions/exposure` (scope `position:read`) is documented in the [API Reference](/api-reference).

## Amend Order

```
POST /orders/{order_id}/amend
Scope: trade:execute
```

Reprices a resting limit order in place, in one venue round trip. The order keeps its Kairos `order_id` **and** its venue `exchange_order_id` — that is the whole point of the endpoint. There is no supersession to record and no second order whose fill you could book twice; the slot in your own book keeps naming exactly one order before and after.

The sequence this replaces — cancel, wait for the venue to confirm, place a replacement — leaves the book empty of your order for the whole cancel round trip and mints an order id you then have to reconcile against the first. An amend has neither window.

Auth: API key or session JWT, scope `trade:execute`. Behind the mutation gate, so a bad API-key triple is a `403`.

### Venue support

Only venues advertising `supports_native_amend` accept an amend. Today that is **Kalshi and nothing else** — read it from [`GET /exchanges/{exchange_id}/capabilities`](/rest/exchanges#get-capabilities) rather than assuming it, and cache it: the flag is a compile-time property of the venue, not of your order, and the handler answers it before it looks at the order's state so that a `false` is permanent rather than a description of this moment.

There is deliberately **no internal cancel-and-replace fallback**. Substituting one for the other changes queue position, order identity, and the failure modes you have to handle, so that substitution stays your decision, made in your code where you can see it — not one made silently underneath you.

### Admission

An amend puts new terms on the book, so it is an execution decision and takes the **same admission checks a submit does**: the venue kill switch, the side/kind/time-in-force circuit breakers, and the order rate limit. The breakers are evaluated against the order's *own* side and kind, because an amend changes neither — the restriction that would have refused the order is the one that refuses moving its price.

That matters more than it looks. Without it, an owner could raise a resting BUY's limit while the venue was halted or sell-only, which is exactly the exposure those breakers exist to stop. An order already being on the book is not standing consent to improve its price.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `order_id` (path) | uuid | Yes | — | A Kairos order UUID you own. A non-UUID value is rejected `400` before the handler runs, and that one response carries a plain-text body rather than the structured error envelope — see [Errors](#errors) |
| `price` | number | No | The order's current price | The new limit price, strictly `> 0` and `< 1`, expressed in the order's **own outcome's** terms. For a Kalshi `No` order send the displayed `No` price; do not complement it client-side. Must also sit on the market's tick grid — see below. Omitted means "re-send the price it already has" |
| `quantity` | number | No | — | **Present only to be refused.** Sending it is a `409` (`EXCHANGE_AMEND_QUANTITY_UNSUPPORTED`) — see the gotcha below |

Unlike [Cancel Order](#cancel-order), this endpoint takes a JSON body. Send `Content-Type: application/json` and at least `{}`; an absent or unparseable body is rejected before the handler runs, with a plain-text body rather than the structured error envelope.

#### The price must be on the tick grid

An off-grid price is rejected `400` (`VALIDATION_INVALID_PRICE`) with a message naming the tick, **before any venue request** — the same grid [`GET /v1/markets/tick-size`](/market-data/markets#tick-size) serves and the same one a submit is held to.

Kalshi's grid is per-market and price-dependent: a tapered market is `0.001` at the tails and `0.01` through the middle. The tick that applies is the one for the band the **new** price lands in, not the band it left, so a reprice that crosses a boundary is judged where it arrives.

<Note>
  **Gotcha:** if the grid cannot be confirmed at that moment, your price is passed to the venue rather than rejected. This is deliberate and it differs from submit, which rejects an unverifiable off-grid price. Your order is already resting: refusing a reprice we merely could not verify would pin you to a stale price for the length of a metadata outage, which is exactly when repricing matters. The venue still refuses a genuinely off-grid price, and that arrives as a `200` with `success: false`.
</Note>

<Note>
  **Gotcha:** `quantity` is not an optional resize, and it is not ignored. This route reprices only, and the field exists so that asking for a resize is a hard error rather than a silent no-op — accepting it and dropping it would let you believe an order had been resized when it had not. The refusal happens *before* the order is even read, so `{"quantity": ...}` against an id that does not exist is still the `409`, not a `404`. To change size, cancel and place a new order.
</Note>

```bash theme={null}
curl -X POST https://execution.kairos.trade/orders/550e8400-e29b-41d4-a716-446655440000/amend \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"price": 0.47}'
```

### Response

```json theme={null}
{
  "success": true,
  "order_id": "550e8400-e29b-41d4-a716-446655440000",
  "exchange_order_id": "b8f3c1a2-4e5d-4f6a-9b0c-1d2e3f4a5b6c",
  "price": "0.47",
  "quantity": "100",
  "message": "Order amended on exchange"
}
```

| Field | Type | Description |
| - | - | - |
| `success` | boolean | Whether the venue confirmed the amend. **Branch on this, not on the status code** |
| `order_id` | uuid | The id you addressed, always — unchanged by the amend |
| `exchange_order_id` | string | The venue's order id, also unchanged. Omitted only on the pre-submission path, where the order does not have one yet |
| `price` | string | Decimal string. The new price on success. On `success: false` it is the price **we last recorded**, which is the resting price whenever the venue explicitly refused — but see the gotcha below for the case where it is only our last known value |
| `quantity` | string | Decimal string, the order's total size. Always the size it was placed for |
| `message` | string | Human-readable outcome detail |

`exchange_order_id`, `price`, `quantity`, and `message` are **omitted** (not `null`) when absent.

<Note>
  **Gotcha:** like [Cancel Order](#cancel-order), this endpoint answers `200` even when the amend did not happen. `success: false` is an **ambiguous outcome, not an error status**: the amend may have been refused, or it may have reached the venue and been applied without us learning so. That ambiguity is exactly why it is not a `5xx` — a `5xx` invites a blind retry against an order that may already carry the new price. A non-amendable status is also a `200` with `success: false`. Non-2xx is reserved for auth, ownership, admission, capability, and input failures.
</Note>

`message` values on `success: false`:

* `"Order cannot be amended - status is filled"` (also `cancelled` / `expired` / `failed`) — the order is terminal. Nothing to reprice; do not retry.
* `"Order is being submitted to exchange. Please try again in a moment."` — the order has no venue identity yet. Retry shortly.
* `"Could not amend on the exchange: <reason>"` — the amend did not complete. This covers an explicit venue refusal, including a price the venue rejects such as one carrying more than four decimal places. It **also** covers a call that failed in transit or came back unreadable, and those two are not distinguishable from the response.

<Warning>
  **Gotcha:** do not read `"Could not amend on the exchange"` as "nothing happened". On an explicit refusal the order is untouched and still resting on its old price. On a timeout or an unreadable reply the venue may have applied the amend anyway, and nothing reconciles that for you — the `price` in the response is then our last known value, not a confirmation of what is resting. **Treat the venue state as unknown and re-read with [`GET /orders/{order_id}`](#get-order) before you act on the price or place anything against that slot.** This is the same discipline a `504` on submit requires, and for the same reason.
</Warning>

### Queue position

Kalshi preserves queue position only when an amend *decreases* size. A reprice — the reason this endpoint exists — forfeits it.

That is not a loss relative to the alternative: the cancel-and-replace you would otherwise run forfeits it too. The win here is the closed off-book window and the stable order identity, not priority. Do not model an amend as a free improvement in the queue.

### Two behaviours worth wiring for

**An amend can cross and fill on the spot.** The amend response never reports that fill — it is not a fill receipt, and `success: true` says the venue accepted the new price, not that the order is still open at it. The fill surfaces where every other fill does, on [`GET /orders/{order_id}`](#get-order) and the `/ws` `fill` event, and you should dedupe it on `fill_id` exactly as you already do. **Re-read after an amend that could cross, and keep re-reading until you have observed the resulting state** — do not assume an amended order is still resting just because the amend succeeded.

**A success can still carry a persistence note.** If the venue confirmed but Kairos could not record the new price, `success` stays `true` and `message` reads `"Order amended on exchange (price not persisted)"`. Treat that literally: the venue has the new price and our copy of it does not. Re-read with [`GET /orders/{order_id}`](#get-order), and do not trust our price field until it agrees.

## Cancel Order

```
POST /orders/{order_id}/cancel
Scope: trade:execute
```

Cancels one order by its Kairos order id. The response reflects a post-cancel re-read, so `filled_quantity` / `avg_fill_price` capture any fill that landed during the round-trip.

Auth: API key or session JWT, scope `trade:execute`. Behind the mutation gate, so a bad API-key triple is a `403`.

If you are cancelling only to re-place the same order at a different price, use [Amend Order](#amend-order) instead where the venue supports it: a cancel-and-replace leaves your order off the book for the whole cancel round trip and gives you a second order id to reconcile, and an amend has neither.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `order_id` (path) | uuid | Yes | — | A Kairos order UUID. A non-UUID value is rejected `400` by the path parser before the handler runs |

No request body.

```bash theme={null}
curl -X POST https://execution.kairos.trade/orders/550e8400-e29b-41d4-a716-446655440000/cancel \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET"
```

### Response

```json theme={null}
{
  "success": true,
  "order_id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Order cancelled on exchange",
  "filled_quantity": "25",
  "avg_fill_price": "0.44"
}
```

<Note>
  **Gotcha:** this endpoint almost always returns `200`, even on failure to cancel. Non-2xx is reserved for auth/ownership/not-found/infrastructure failures — a `success: false` body with a `message` covers an already-terminal order or a venue that currently refuses the cancel (still live — retry). Branch on `success`, not the status code.
</Note>

`filled_quantity` / `avg_fill_price` are **omitted** (not `null`) when nothing filled, and on the early-return paths — already-terminal, cancel-race, pre-exchange cancel — where the value would be unknown.

Example `message` values: `"Order cancelled on exchange"` and `"Order cancelled (pre-exchange)"` on success; when `success` is `false`, `"Order cannot be cancelled - status is Filled"` (also `Cancelled` / `Expired`), `"Order already failed"`, `"Order is being submitted to exchange. Please try again in a moment."`, or `"Could not confirm cancellation on the exchange: <reason>"` — the last one means the order may still be live, so retry.

## Batch Cancel Selected Orders

```
POST /orders/cancel-batch
Scope: trade:execute
```

Cancels a specific list of order ids. **The whole selection is validated before any venue request is sent** — every order must exist, belong to you, be non-terminal, have an exchange order id, and share one `exchange_id`. If validation fails, nothing is cancelled. Supported on Polymarket, Kalshi, Predict.fun, and Hyperliquid; other venues return `400`.

Auth: API key or session JWT, scope `trade:execute`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `order_ids` | string\[] | Yes | — | Max 100, deduped server-side. Kairos order UUIDs |

```bash theme={null}
curl -X POST https://execution.kairos.trade/orders/cancel-batch \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "order_ids": [
      "550e8400-e29b-41d4-a716-446655440000",
      "660e8400-e29b-41d4-a716-446655440000"
    ]
  }'
```

### Response

```json theme={null}
{
  "success": true,
  "cancelled_count": 2,
  "noop_count": 0,
  "failed_count": 0,
  "cancelled_order_ids": [
    "550e8400-e29b-41d4-a716-446655440000",
    "660e8400-e29b-41d4-a716-446655440000"
  ],
  "noop_order_ids": [],
  "failures": []
}
```

`noop_count` covers orders the venue reports as already terminal or absent — still handled, not a failure. **Fill-during-cancel noops are never counted as cancelled**; fill reconciliation remains authoritative. Venue-side partial failures land in `failures` and are not marked cancelled locally.

Validation failures are bare statuses with an empty body: `404` if any id is unknown, `403` if any order belongs to someone else, **`409` if any order is already terminal or has not reached the venue yet**, and `400` for an empty list, more than 100 ids, a mixed-exchange selection, or a venue with no batch cancel. A venue-side failure of the batch call itself is `500`.

## Cancel All Orders

```
POST /orders/cancel-all
Scope: trade:execute
```

Kill-switch endpoint: cancels every open order on `exchange_id` (optionally scoped to `market_id`) via the venue's native cancel-all, then reconciles local rows up to whatever the venue reports cancelled.

Auth: API key or session JWT, scope `trade:execute`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `exchange_id` | string | Yes | — | Exchange to cancel on |
| `market_id` | string | No | — | Restrict to a specific market |
| `user_id` | string | No | — | **Ignored** — authenticated user is always used |

```bash theme={null}
curl -X POST https://execution.kairos.trade/orders/cancel-all \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "exchange_id": "polymarket",
    "market_id": "570362"
  }'
```

### Response

```json theme={null}
{
  "cancelled_count": 3,
  "failed_count": 0,
  "cancelled_order_ids": ["550e...", "551e...", "552e..."]
}
```

`cancelled_count` is the venue's authoritative count. If the venue reports `0`, no local rows are touched — the GTC poller / fill workers reconcile from there rather than the endpoint blindly marking rows cancelled.

<Note>
  **Gotcha:** unlike single/batch cancel, a caller-input error here is a real `400`, not a `success: false` 200. This is the kill-switch path, so a bad request must read differently from "the venue kept orders resting."
</Note>

### Per-venue semantics

| Venue | `market_id` filter accepts | Mechanism |
| - | - | - |
| Polymarket | Numeric market id or `0x…` condition id | Native venue cancel-all |
| Kalshi | Raw Kalshi ticker | Native venue cancel-all |
| Predict.fun | Numeric market id only — a hex/non-decimal value is rejected `400` before any venue call | **Synthetic**, see below |

**Predict.fun cancel-all is synthetic and non-atomic.** The venue has no native cancel-all, so Kairos lists all open orders (paginated), applies the optional market filter, and cancels in batches of ≤100 (the venue's per-request cap):

* **Fill-during-cancel race** — an order can fill between listing and its cancel batch; the venue reports it as a no-op, never counted in `cancelled_count`.
* **Partial success is possible** — one batch can succeed while a later one fails, returning `PREDICTFUN_CANCEL_ALL_PARTIAL` with how many were already cancelled; retrying sweeps the remainder.
* **A failed or incomplete listing never cancels** — a failed listing errors instead of reporting "nothing to cancel"; exceeding the 2,000-order pagination safety cap (20 pages × 100) fails with `PREDICTFUN_CANCEL_ALL_LIST_OVERFLOW` rather than sweeping partial data.

Both are venue codes, so they arrive as `error_details.metadata.code` on a `502` whose `error_details.code` is `EXCHANGE_ERROR` — match on the metadata code, not the top-level one.

## Fee Quote

```
GET /orders/fee-quote
Scope: trade:read
```

Prices a trade before you submit it — a market order (`order_type=market`, `price` omitted) walks the live book for `quantity`; a limit order (`order_type=limit`) requires `price` as the resting quote. Shares the exact computation used by the WebSocket [Fee Quote (RFQ)](/websocket/fee-quote) stream, so the two surfaces never disagree.

Auth: API key or session JWT, scope `trade:read`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `exchange_id` | string | Yes | — | `polymarket`, `kalshi`, or `predictfun` (`kalshi_offchain` accepted as an alias) |
| `quantity` | string | Yes | — | > 0. Decimal string, shares/contracts |
| `side` | string | Yes | — | `buy` or `sell`, case-insensitive. The quote walks the ask side for buy, bid side for sell |
| `order_type` | string | Yes | — | `market` or `limit`, case-insensitive |
| `market_id` | string | One of `market_id`/`token_id` | — | Condition id (Polymarket). Required for Polymarket if `token_id` omitted |
| `token_id` | string | One of `market_id`/`token_id` | — | CLOB token id (Polymarket) — preferred over `market_id` |
| `price` | string | Limit only | — | `(0, 1]`. Resting price; required when `order_type=limit`. **Ignored for `market`** — the book is always walked |

```bash theme={null}
curl "https://execution.kairos.trade/orders/fee-quote?exchange_id=polymarket&market_id=570362&side=buy&order_type=market&quantity=100" \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET"
```

Every validation failure here is a bare `400` with an empty body (unsupported exchange, bad `side`/`order_type`, non-positive `quantity`, `price` outside `(0, 1]`, a limit quote with no `price`, or a Polymarket quote with neither id). A missing scope or a disabled provider is `403`.

### Response

```json theme={null}
{
  "avg_price_usdc": "0.452000",
  "filled_size": "100",
  "requested_size": "100",
  "sufficient_liquidity": true,
  "notional_usdc": "45.200000",
  "platform_fee_usdc": "2.750000",
  "exchange_fee_usdc": "0.180000",
  "venue_reserve_fee_usdc": "0.180000",
  "total_fee_usdc": "2.930000",
  "exchange_fee_note": "0.08% taker fee",
  "total_cost_usdc": "48.130000",
  "pricing_unavailable": false,
  "is_estimate": true
}
```

| Field | Type | Description |
| - | - | - |
| `avg_price_usdc` | string | Price per share on the 0–1 scale (despite the `usdc` suffix): VWAP of the executable slice for a market order, or the limit price for a limit order |
| `exchange_fee_note` | string\|null | Human-readable fee description; `null` when the venue has none |
| `sufficient_liquidity` | boolean | `false` when the book can't fill `requested_size` |
| `venue_reserve_fee_usdc` | string | What the venue actually reserves to accept the order — can exceed `exchange_fee_usdc` (Polymarket CLOB reserves the taker estimate on a resting limit order since it can't know it'll stay maker). **Size affordability checks off this field, not `exchange_fee_usdc`.** |
| `total_cost_usdc` | string | All-in: buy → notional + fees; sell → notional − fees |
| `pricing_unavailable` | boolean | `true` means no executable price was found — every numeric field is a `"0"` placeholder and **must not be rendered as a real quote** |
| `is_estimate` | boolean | Always `true` — authoritative fee is computed at fill time |

<Note>
  **Gotcha:** the response is `200` even when no price could be found. Check `pricing_unavailable` before rendering a quote.
</Note>

## Order Status Types

| Status | Description | Terminal |
| - | - | - |
| `pending` | Accepted and not yet at the venue. Covers the internal `queued`/`locked`/`orphaned` states, which are persisted as `pending` | No |
| `executing` | A worker has claimed the order and is submitting it to the venue. Persisted and returned by `GET /orders` / `GET /orders/{order_id}` until the venue acknowledges (then `live`) or the attempt fails | No |
| `live` | Active on the exchange | No |
| `partial` | Some quantity filled. Active while a resting order remains live, but it may be terminal after an immediate remainder is killed or after cancellation/expiry. | Depends on venue state |
| `filled` | Fully filled | Yes |
| `cancelled` | Cancelled by user | Yes |
| `expired` | GTD order reached expiration | Yes |
| `failed` | Could not execute — see `error_message` | Yes |

For market orders and explicit immediate TIFs (`FAK`, `IOC`, and `FOK`), a
venue kill with zero fills is `failed`, not `cancelled`. A partial immediate
execution is terminal, but reconciliation may persist it as either `partial`
or `filled`.

<Warning>
  **Gotcha:** treat `status` as authoritative. Do not infer completion by comparing `filled_quantity` with `quantity`: `quantity` is the gross requested size, while Predict.fun BUY fills record the net shares received after share-denominated fees. A fully executed order can therefore have `filled_quantity < quantity`.
</Warning>

## Errors

Order submission and cancel-all return a structured body:

```json theme={null}
{
  "error": "Quantity must be positive",
  "code": "ValidationInvalidSize",
  "error_details": {
    "code": "VALIDATION_INVALID_SIZE",
    "message": "Quantity must be positive",
    "actions": []
  }
}
```

`error_details.details` and `error_details.metadata` are **omitted when absent**, never `null`. `actions` is always present (possibly empty); each entry is `{ "action", "label", "primary" }` with a snake\_case `action` such as `review_order`, `retry`, `add_funds`, `enable_trading`, `contact_support`.

<Note>
  **Gotcha:** `code` and `error_details.code` are not the same casing. Top-level `code` is a legacy PascalCase debug string (`ValidationInvalidSize`); `error_details.code` is the canonical SCREAMING\_SNAKE\_CASE wire code (`VALIDATION_INVALID_SIZE`) — new integrations should match on `error_details.code`.
</Note>

**Only `POST /orders`, `POST /orders/cancel-all`, and `POST /orders/{order_id}/amend` return this envelope, and only from the handler itself** — a request rejected before the handler runs (a non-UUID `{order_id}`, a missing or unparseable JSON body) answers with a plain-text body and no `error_details`, so a client that assumes the envelope on every `400` will misparse those two cases. `GET /orders`, `GET /orders/{order_id}`, `GET /orders/fee-quote`, single cancel and batch cancel return a bare status with an empty body — rely on the status code alone there. Failures raised by the auth layer itself (on any endpoint) return `{"error": "<generic message>"}` with no `error_details`.

The Code column below holds the `error_details.code` value where this page names one for that status, and `—` where the response carries no documented code.

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `400` | `VALIDATION_INVALID_SIZE`, `VALIDATION_INVALID_ORDER`, `EXCHANGE_UNSUPPORTED`, `FUNDS_INSUFFICIENT_USDC`, `FUNDS_INSUFFICIENT_BALANCE` | Invalid input — bad `side`/`kind`, missing or out-of-range `quantity`/`price`, quantity below `MIN_ORDER_QUANTITY` on a buy or above 1,000,000, API-key buy under the \$5 notional minimum, unrecognised `time_in_force`, `expiration_minutes` outside `1`–`43200` on a GTD, `max_slippage`/`max_slippage_cents`/`max_retries` out of range, a TIF or `post_only` the venue does not support, an unrecognised `collateral` mode, `collateral: "fund"` missing `max_bridge_fee_usdc` or `max_funding_wait_ms`, either cap sent without `collateral: "fund"`, unsupported `exchange_id` (`EXCHANGE_UNSUPPORTED`), insufficient balance (`FUNDS_INSUFFICIENT_USDC`) or position shortfall (`FUNDS_INSUFFICIENT_BALANCE`), a closed market, slippage exceeded, a FOK that could not fill, empty/oversized/mixed-exchange batch cancel, malformed `market_id` on Predict.fun cancel-all, unknown `exchange_id` on cancel-all, an amend of a non-limit order, to a price outside `(0, 1)`, or to a price off the market's tick grid (`VALIDATION_INVALID_PRICE` — the message names the tick; resubmit at a multiple of it). **Without the envelope:** a non-UUID `{order_id}`, or a missing/unparseable JSON body on amend | Fix the request and resend — retrying it unchanged returns the same error. On the funds codes, add collateral or reduce size first |
| `401` | — | Missing/invalid/expired/revoked auth on a read endpoint | Re-authenticate; check the API-key triple or refresh the session JWT |
| `403` | `AUTH_INSUFFICIENT_SCOPE`, `AUTH_CREDENTIALS_INVALID` | Missing required scope (`AUTH_INSUFFICIENT_SCOPE`), API-key access disabled for that venue, source IP not on the key's allowlist, trading not enabled or no Turnkey identity for the caller (`AUTH_CREDENTIALS_INVALID`), an order owned by another user, or a failed API-key triple on a mutating endpoint | Not retryable as sent. Grant the scope, allowlist the IP, enable trading, or stop addressing another user's order. On a mutating endpoint, check the triple — a bad one is `403` here, not `401` |
| `404` | `VALIDATION_MARKET_NOT_FOUND`, `VALIDATION_INVALID_ORDER` | Order not found, or the market could not be resolved. Amend returns `VALIDATION_INVALID_ORDER` here, and returns it for an order owned by someone else too rather than confirming the id exists | Verify the order id or `market_id`; resend only with a corrected identifier |
| `409` | `EXCHANGE_AMEND_UNSUPPORTED`, `EXCHANGE_AMEND_QUANTITY_UNSUPPORTED` | Batch cancel selected an order that is already terminal or has not reached the venue (no code). On [amend](#amend-order): the venue has no native amend (`EXCHANGE_AMEND_UNSUPPORTED`), or the request carried a `quantity` (`EXCHANGE_AMEND_QUANTITY_UNSUPPORTED`) | Batch cancel: re-read the orders, drop the terminal/not-yet-placed ids, and resubmit. Nothing was cancelled. Amend: `EXCHANGE_AMEND_UNSUPPORTED` is permanent for that venue — fall back to cancel-and-replace and stop asking; `EXCHANGE_AMEND_QUANTITY_UNSUPPORTED` means drop the field, since this route never resizes |
| `422` | `ORDERBOOK_UNAVAILABLE` | Market order couldn't be priced — live orderbook missing or stale | Retry once the book is available, or submit a limit order with an explicit `price` |
| `429` | `VALIDATION_INVALID_ORDER` | Per-user (or per-key override) order rate limit exceeded, a venue rate limit relayed back, or too many failed auth attempts from one IP | Back off and retry. Match on the status, not the code. Idempotent replays with a matching `client_order_id` do not consume a slot |
| `500` | `DATABASE_ERROR`, `INTERNAL_ERROR` | Internal failure (DB error, signing error, pre-trade failure, unresolved cancel) | Re-read the order with `GET /orders/{order_id}` before resubmitting; report to support with the response body |
| `502` | `EXCHANGE_ERROR`, `NETWORK_ERROR` | Venue rejected the order or the exchange call failed | Read `error_details.code` (and `error_details.metadata.code` on Predict.fun cancel-all) before retrying — a venue rejection will repeat, a network failure may not |
| `503` | `MARKET_PAUSED`, `INTERNAL_ERROR`, `EXCHANGE_ERROR`, `AUTH_CREDENTIALS_NOT_FOUND` | Execution disabled by a circuit breaker or lock contention (`MARKET_PAUSED`), or the API-key provider-access lookup was unavailable (`INTERNAL_ERROR`). On [amend](#amend-order) also: no executor registered for the venue (`EXCHANGE_ERROR`) or venue credentials could not be loaded (`AUTH_CREDENTIALS_NOT_FOUND`) | Retry later; nothing was placed or amended. Every `503` here is retryable — a permanent authorization failure is a `403`, never a `503` |
| `504` | `NETWORK_TIMEOUT` | Upstream timeout | The order may still have reached the venue — re-read with `GET /orders/{order_id}` (or resend with the same `client_order_id`) rather than blindly resubmitting |

<Warning>
  **Gotcha:** two entries above read oddly. A **429 rate limit carries `error_details.code = VALIDATION_INVALID_ORDER`** (match the status, not the code), and **insufficient funds is a `400`, not a `402`** — this service never returns `402`.
</Warning>

On [amend](#amend-order) the codes you will actually see are `EXCHANGE_AMEND_UNSUPPORTED`, `EXCHANGE_AMEND_QUANTITY_UNSUPPORTED`, `VALIDATION_INVALID_ORDER`, `VALIDATION_INVALID_PRICE`, `AUTH_INSUFFICIENT_SCOPE`, `MARKET_PAUSED`, `EXCHANGE_ERROR`, `AUTH_CREDENTIALS_NOT_FOUND`, and `INTERNAL_ERROR`. A venue that simply refused the amend is not in this list — that is a `200` with `success: false`.

Common `error_details.code` values on order submit: `VALIDATION_INVALID_SIZE`, `VALIDATION_INVALID_PRICE`, `VALIDATION_INVALID_ORDER`, `VALIDATION_MARKET_NOT_FOUND`, `EXCHANGE_UNSUPPORTED`, `EXCHANGE_ERROR`, `FUNDS_INSUFFICIENT_USDC`, `FUNDS_INSUFFICIENT_BALANCE`, `ALLOWANCE_CTF_NOT_SET`, `MARKET_PAUSED`, `MARKET_NOT_READY`, `MARKET_INSUFFICIENT_LIQUIDITY`, `MARKET_FOK_NOT_FILLED`, `ORDERBOOK_UNAVAILABLE`, `AUTH_CREDENTIALS_INVALID`, `AUTH_CREDENTIALS_NOT_FOUND`, `AUTH_INSUFFICIENT_SCOPE`, `AUTH_POLYMARKET_API_KEY_INVALID`, `SIGNATURE_ERROR`, `NETWORK_ERROR`, `NETWORK_TIMEOUT`, `DATABASE_ERROR`, `INTERNAL_ERROR`.


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