Skip to main content
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.
Submit, cancel, and query orders on the custodial lane, where Kairos signs and routes on your behalf — see External Execution 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.

Base URL

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

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

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.
Gotcha: API-key BUYs have a 5minimumnotional.‘quantity×price‘below‘5 minimum notional. `quantity × price` below `5is rejected400 (VALIDATION_INVALID_ORDER`). The rule applies only to BUYs authenticated with an API key — session-JWT callers and all SELLs are exempt.

Response

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

Time-in-Force

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.

Get Order

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

Response

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

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

Response

Order rows in the same shape as 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.

Amend Order

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

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

Response

exchange_order_id, price, quantity, and message are omitted (not null) when absent.
Gotcha: like 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.
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.
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} 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.

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} 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}, and do not trust our price field until it agrees.

Cancel Order

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

No request body.

Response

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

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

Response

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

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

Response

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

Per-venue semantics

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

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) stream, so the two surfaces never disagree. Auth: API key or session JWT, scope trade:read.

Request

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

Gotcha: the response is 200 even when no price could be found. Check pricing_unavailable before rendering a quote.

Order Status Types

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

Errors

Order submission and cancel-all return a structured body:
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.
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.
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.
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.
On amend 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.