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.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.
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
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 5
is rejected400 (VALIDATION_INVALID_ORDER`). The rule applies only to BUYs authenticated with an API key — session-JWT callers and all SELLs are exempt.Response
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 asmarket_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
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
Whenstatus is failed, failure carries { code, classification, details }:
classificationis 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.actionsis a UI affordance only. It is derived fromcodeindependently ofclassificationand may includeretry— even as the primary action — on anon_retryablefailure. Do not build an automatic retry loop fromactions; branch onclassification.codeis the execution error’s wire code (e.g.FOK_NOT_FILLED);details.codeis theOrderErrorCodeused for grouping (e.g.MARKET_FOK_NOT_FILLED). For a venueExchangeErrorthe two coincide.
List Orders
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.rawandmetadataare stripped tonullon every row to keep this cheap to poll (rawalone can be ~100 KB/order) — theoutcomelabel is preserved by falling back tometadata.outcomebefore stripping. Fetch a single order viaGET /orders/{order_id}for the full row.
Need current position exposure rather than order state?GET /positions/exposure(scopeposition:read) is documented in the API Reference.
Amend Order
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 advertisingsupports_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 rejected400 (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"(alsocancelled/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.
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, andsuccess: 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
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
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
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_PARTIALwith 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_OVERFLOWrather than sweeping partial data.
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
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
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.
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.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.
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.
