Execution

Place, read, and cancel orders through OpenMarkets against a venue you name — for your own account (player JWT) or, with Connect, for each of your users (act-as).

Mental model

Execution turns a position you found in the read API into a fill on a specific venue. The liquidity feed for a contest returns each bettable outcome once, with a partner_liquidities[] array — one entry per venue that prices it, each carrying that venue's price, available size, and a liquidity_hash. You pick the venue you want, post its liquidity_hash and a price ceiling, and OpenMarkets places the underlying order on that partner on the acting account's behalf.

The pieces:

  • position_hash — venue-agnostic outcome key (the outcome, not a venue). To execute against it today you must also name a venue with partner_id.
  • liquidity_hash — a partner-scoped price key from partner_liquidities[]. It already names the venue, so it's the simplest thing to execute against.
  • router_order — the order we placed on that partner on the acting account's behalf.
You name the venue. Today execution is venue-directed: every leg resolves to one partner you chose — either a liquidity_hash, or a position_hash plus an explicit partner_id. Automatic best-venue routing across partners is coming soon — see below.

Auth & scope

Every execution endpoint accepts any of these caller identities:

  • Player JWT — the user acts on their own account. Send Authorization: Bearer <jwt>.
  • Org key, own account (owner_self) — a first-party desk trading its own book. Send X-API-Key with an execution-tier key and no X-OpenMarkets-Account header. The order books against the key's own account — the router_account_id reported by GET /auth/me — exactly like every /auth/* read you already make.
  • Connect org key, act-as — you act on one of your users. Send X-API-Key plus X-OpenMarkets-Account: <your-user-id> to scope the call to that user's sub-account. See the Connect integration guide.

Reads (GET orders, resting, resolution) need the account_read scope. The writes — /orders/buy and /orders/cancel — require an execution-tier key with the trade:execute scope (or a player JWT acting on itself). An account_read-only key that tries to place an order gets 403.

Regardless of who calls, the order always books against the acting account — the player's own account on a JWT, the org's own account when no X-OpenMarkets-Account is sent (owner_self), or the Connect sub-account named by X-OpenMarkets-Account. An org can never place an order against a user it doesn't own (403 account_forbidden).

Base URL

text
https://api.openmarkets.ai/flow/v1/auth

All examples below use Authorization: Bearer ... (player JWT). For Connect, swap it for X-API-Key + X-OpenMarkets-Account — the request and response bodies are identical.

Place an order

POST /flow/v1/auth/orders/buy is the single execution entry point. It is multi-leg native — the body is an orders array and each leg is placed independently, so you can fill several positions (or the two legs of an arb) in one call. Each leg names its own venue.

The simplest form passes a liquidity_hash straight from partner_liquidities[] — it already names the venue:

bash
curl -X POST https://api.openmarkets.ai/flow/v1/auth/orders/buy \
  -H 'Authorization: Bearer eyJhbGc...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 3f9c1e5a-...' \
  -d '{
    "orders": [
      {
        "liquidity_hash": "kalshi:ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",
        "amount": 50,
        "max_price": 0.52
      }
    ]
  }'

Equivalently, name the venue explicitly with a position_hash + partner_id:

json
{
  "orders": [
    {
      "position_hash": "ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",
      "partner_id": "kalshi",
      "amount": 50,
      "max_price": 0.52
    }
  ]
}

Each leg:

  • Exactly one of position_hash or liquidity_hash.
  • partner_id — the venue to fill on. Required with a bare position_hash today; already implied by a liquidity_hash. (Omitting it to let OpenMarkets pick the best venue is coming soon.)
  • Exactly one of amount (stake in USD) or shares (contract count). Must be positive.
  • max_price — price ceiling, strictly between 0 and 1. The order won't fill above it.
  • mode (optional)"real" (default) or "paper" for a practice fill against the internal book.
  • order_type (optional) "market" (default) or "limit". market fills immediately against resting liquidity up to your max_price and cancels any remainder — it never rests. limit posts a resting order at max_price that stays on the book until it fills or you cancel it — including below the current best price (a limit priced under the ask rests and waits; it is not rejected for lack of takeable liquidity, unlike a market order). Not every venue supports limit — see each venue's supported_order_types.
The body is strict. Any field outside this contract — a typo, an extra key, a stray leg field — returns 400 unknown_parameter and nothing is placed. There is no silent ignore.
Asking a venue for an order type it can't honor (e.g. order_type: "limit" on an immediate-only venue) returns 400 unsupported_order_type and nothing is placed — on both /orders/buy and /orders/preflight, so you can check support before charging the user.
Optional verify_liquidity: true (top-level). Before routing, confirm each named venue's live orderbook when its cached liquidity is stale or thin. If the book has moved away, that leg comes back status: "rejected" with rejected_code: "insufficient_live_depth" — declined before it fires, rather than sent as an order that fails to fill. Fresh, deep venues skip the extra check.

This declines the one leg; it does not stop the others. On a multi-leg request the remaining legs still place unless you also send check_all_first: true and on_leg_failure: "halt" — see multi-leg behavior. To preview all of this (routing + fees) without placing anything, use /orders/preflight below.
Group orders with opportunity_id (top-level). By default every request opens its own opportunity. Pass an existing one and these orders join it — which is what you want when a hedge came out one-legged and you're placing the offsetting order: it has to sit beside the leg that created the exposure, not read as an unrelated position. Must be an opportunity you own (403 opportunity_not_found) and match its currency (400 currency_mismatch — a real order can't be filed into a practice one).

Attribute with model_id (top-level). Records the model on every order and on the opportunity. Must be a model you own — 404 model_not_found.

The response reports each leg by index, with its realized fill on the named venue:

Response.data
{
  "results": [
    {
      "index": 0,
      "status": "completed",           // "completed" (filled) | "failed" (placed/blocked, no fill) | "rejected" (planner refused)
      "sor_decision_id": "sor_...",     // execution audit id
      "requested_partner_id": "kalshi", // the venue you named (from partner_id, or the liquidity_hash's first segment)
      "filled_notional": 1.89,          // USD ACTUALLY booked (whole-contract venues round down)
      "filled_shares": 9,               // contracts/shares actually filled
      "unfilled_notional": 0.11,        // requested minus booked (0 on a clean full fill)
      "avg_price": 0.21,
      "rejected_reason": null,          // human-readable reason when not "completed"
      "rejected_code": null,            // machine code: "venue_disabled" | "execution_limit_exceeded"
                                        //   | "above_max_price" | "insufficient_size"
                                        //   | "insufficient_live_depth" | "insufficient_balance"
                                        //   | "halted_upstream"  (never placed — see Multi-leg)
      "venues": [
        {
          "partner_id": "kalshi",
          "success": true,
          "router_order_id": "ro_...",  // the placed order — read it back via GET /orders
          "error": null
        }
      ]
    }
  ],
  "halted_at_index": null              // index that stopped the request under
                                       // on_leg_failure:"halt"; null = ran to completion
}
filled_notional / filled_shares are the ACTUAL booked fill, matching the order in GET /orders — not your requested stake. On whole-contract venues (Kalshi, Polymarket) the venue rounds down to whole contracts, so a $2 request at 21¢ books 9 contracts = $1.89, and unfilled_notional reports the un-buyable 0.11 remainder. A leg can also partially fill; either way, use unfilled_notional to tell a full fill from a rounded-down/partial one without a second request. By default legs are independent — one can complete while another is failed. See multi-leg requests to stop at the first failure instead.

The status values are distinct:

  • completed — the leg reached the venue and filled (fully, or — for a market order — partially with the remainder killed; check filled_notional). Terminal.
  • resting — a limit order placed with an unfilled remainder live on the book (unfilled_notional > 0).Non-terminal — this 200 is the initial state; watch for the fill on the orders WebSocket channel (or poll GET /orders/resting/me). Cancel it with /orders/cancel.
  • failed — the leg was placed or blocked but did not fill: a venue error, a fill-or-kill that found no liquidity at your max_price, or an execution-control block (see below — carries a rejected_code).
  • rejected — the router refused to plan the leg before dispatch (no viable venue for the position, or an invalid amount). Carries a free-text rejected_reason only.

Multi-leg requests

orders takes as many legs as you need. They are attempted one at a time, in the order you send themorders[0] fully completes (reaching the venue and returning a fill) before orders[1] starts. results comes back index-aligned with your request.

Array order is your lever. If one leg should be attempted first, put it first — there is no other way to express priority. This matters most with on_leg_failure: "halt", where the first leg is the one that decides whether the rest happen at all: lead with the leg you most expect to fail (the thinner book, the flakier venue, the tighter price) so you find out before committing capital to the others.

Two intents, one field:

  • Maximize fills (default, on_leg_failure: "continue") — every leg is attempted regardless of what happened to the others. What you want when the legs stand on their own: a model firing several independent positions, where a miss on one shouldn't cost you the rest.
  • Stop at the first failure (on_leg_failure: "halt") — no further leg is placed. What you want when a later leg only makes sense if the earlier one landed: an arbitrage pair, a hedge, a staged entry.
Stop if the first leg doesn't land
{
  "on_leg_failure": "halt",
  "orders": [
    { "liquidity_hash": "kalshi:...",   "amount": 100, "max_price": 0.48 },
    { "liquidity_hash": "prophetx:...", "amount": 100, "max_price": 0.50 }
  ]
}

A leg halts the request when its status is failed or rejected. Legs that were never attempted come back status: "rejected" with rejected_code: "halted_upstream", so results stays index-aligned with your request and you can tell “never placed” from “placed and failed” without diffing your own payload. halted_at_index names the leg that stopped it.

Note what this means for the two non-terminal statuses: resting and pending are successes and do not halt. A resting limit is doing exactly what a limit order is for, so halting on it would stop nearly every request containing one.

Halt is not atomicity, and not a rollback. It stops future legs. Anything already placed stays placed and is yours to unwind — with /orders/cancel for a resting order, or an offsetting order for a filled one. To reduce the chance of getting there at all, add verify_liquidity: true AND check_all_first: true — see below. Depth-checking alone does not stop anything: a declined leg is skipped and the rest still place.

Bail before anything is placed — check_all_first: true. Runs every leg's pre-placement checks up front: live orderbook depth, and whether the batch is actually affordable at each venue. The checks run concurrently, so it costs one extra round-trip in total, not one per leg.

check_all_first does not bail on its own — it needs on_leg_failure: "halt" too. By itself it only collects the failures; on_leg_failure defaults to "continue", so the failing legs are skipped and every other leg is still placed. Sending check_all_first alone for a two-leg hedge gets you the half-hedge it looks like it prevents. If you want zero exposure, send both.
All-or-nothing entry
{
  "check_all_first": true,
  "on_leg_failure": "halt",
  "orders": [ legA, legB ]
}
// any leg fails a check → NOTHING is placed, zero exposure
// with the default "continue" → failing legs are skipped, the rest still go
This is the only setting that prevents a paired-intent failure. halt reacts to a leg that already failed — but the classic batch failure is leg A succeeding and leg B failing for depth or funds, and no amount of halt logic prevents that. Only checking every leg before committing capital does. The cost is latency before your first placement, which on a fast-moving price is its own risk — so it's a per-request choice, not a default.

Dispatch everything at once — placement: "parallel". Legs go simultaneously instead of one at a time, minimizing the window where one leg is filled and another isn't. Legs on different venues run genuinely concurrently; same-venue, paper and market-making legs still serialize where they share account state. Pair it with check_all_first AND on_leg_failure: "halt" for the tightest arbitrage entry available: verify every leg, abort the whole batch if any fails, otherwise fire them together.

placement: "parallel" with on_leg_failure: "halt" and NO check_all_first returns 400 conflicting_parameters. Once every leg is dispatched there is nothing left to halt — we reject it rather than silently ignore half of what you asked for.

With check_all_first: true the combination is allowed — that halt fires before any leg is dispatched, so there is nothing parallel about it to defeat. This is the recommended shape for a hedge.
“Parallel” changes dispatch, not the response. The HTTP call is synchronous either way — you always get every leg's result in the 200. There is no fire-and-forget or 202 mode. What placement controls is whether legs are sent to their venues one after another or all at once. The only genuinely asynchronous thing here is a resting limit leg, whose later fills arrive on the orders WebSocket channel regardless of which mode you chose.
The balance check is a filter, not a guarantee. It compares your batch against the account's last-known balance at each venue, summing legs per venue since they draw on one balance. It fails open: if we have no synced balance for a venue, the batch proceeds and the venue remains the source of truth. It catches the obvious footgun — sending five orders you can only afford three of — not every funding failure.
pending carries real risk under halt. On async-fill venues (Polymarket, PolymarketUS) a market order is accepted and its fill confirms a beat later via reconcile, so the leg reports pending rather than completed. Halt treats that as success and places the following legs. If that fill never lands, the later legs were placed against a leg that never filled. Watch the orders WebSocket channel to confirm, and be ready to unwind.

Choosing a combination. Three independent props, but only a few pairings are worth reaching for. A blank cell means “leave it out”:

You wantplacementon_leg_failurecheck_all_first
Max fills — legs stand alone
Stop if a leg misseshalt
Fire everything at once, monitor yourselfparallel
Zero exposure on any pre-check failurehalttrue
Arbitrage / hedge — zero exposure AND tightest entryparallelhalttrue

The last row is what a hedge wants, and it is worth being precise about what each prop buys you. check_all_first + halt decides whether to enter at all — nothing is placed if any leg fails its check. placement: "parallel" then decides how fast the legs go out once that decision is yes, minimizing the window where one has filled and the other hasn't. They are independent, and you want both.

None of this is atomic across venues. Every combination above still allows leg A to fill and leg B to be rejected at the venue, after the checks passed. The pre-placement checks shrink that window; they cannot close it, because two venues cannot be made to commit together. Always confirm the actual fills on the orders WebSocket channel or GET /orders, and be ready to unwind. A leg reporting pending or resting has not filled yet — only completed, failed and rejected are final.

Preflight — dry-run routing + expected fees

POST /flow/v1/auth/orders/preflight is a dry run of /orders/buyidentical request body, but it places nothing. It runs the router against live liquidity (pinned to the venue you name, exactly like /buy) and tells you, per leg: whether the depth is there, the expected fill price, and the expected taker/maker fees for your actual amount — plus the all-in cost. Use it to show a confirm screen before charging the user.

Preflight vs check_all_first. They answer the same question at different moments, and you want different ones for different jobs. /preflight is a separate call — use it to show a user what an order will cost, or to explore, with a human deciding in between. The price can move between it and your /buy. check_all_first is the same class of check inside the buy request, so there is no gap for a human or a market to move in — but it reports nothing back unless a leg is declined. Fee previews and confirm screens → preflight. Automated all-or-nothing entry → check_all_first.
Response.data
{
  "results": [
    {
      "index": 0,
      "requested_partner_id": "kalshi",
      "accepted": true,                 // can it fill as planned?
      "rejected_reason": null,          // e.g. "no_viable_venues" when it can't
      "expected_fill_price": 0.52,
      "total_allocated": 50.0,          // USD notional that would fill
      "unfilled_amount": 0.0,           // notional that can't fill (thin book)
      "total_taker_fee": 1.44,          // expected fee for THIS order (not $100-canonical)
      "total_maker_fee": 0.0,
      "total_cost_taker": 51.44,        // notional + taker fee = all-in cost
      "venues": [
        {
          "partner_id": "kalshi",
          "expected_price": 0.52,
          "allocated_amount": 50.0,
          "fee_packet": { "taker_price": 0.5344, "maker_price": 0.52, "taker_fee": 1.44, "maker_fee": 0.0 }
        }
      ]
    }
  ]
}
Two fee surfaces, one model. The liquidity feed's per-venue fee_packet gives you fee-adjusted rates taker_price/maker_price are usable at any size, but its taker_fee/maker_fee are on a canonical $100 stake (a reference, not your order's fee). Preflight returns the same fee_packet shape but computed on your actual amount, so it's the number to show the user. Fees are estimates; the venue's realized fee is recorded on the order after the fill (fee / amount_with_fee).
Confirm live depth with verify_liquidity: true. By default preflight routes against our liquidity cache (seconds fresh — the same data /buy uses). Set verify_liquidity: true and, for any venue whose cached entry is stale or thin (shallower than ~3× your order), we hit that venue's live orderbook to confirm real depth — returning verified: true with the live live_available and live_price on the venue, and declining the leg (rejected_reason: "insufficient_live_depth") if the book has moved away. Fresh, deep venues skip the extra call.

Preflight needs only account_read (it's a preview — no trade:execute, no Idempotency-Key, no order placement).

Idempotency

Send an Idempotency-Key header (a UUID you generate per logical order) on every /orders/buy. If a call times out or you retry, the same key prevents a duplicate fill. Use a fresh key for each new intent to place an order.

Execution controls

Before any leg is placed, OpenMarkets enforces the acting account's controls:

  • Venue must be live — the targeted partner must be connected and active for that user. A paused or unconnected venue blocks the leg with the code venue_disabled.
  • Per-account limits — the user's execution-limit policy (max stake, per-venue, per-league, practice-only) is evaluated per leg. A violation blocks the leg with execution_limit_exceeded.

Controls run before the order reaches the venue, so a leg can never bypass the user's limits. Users set these themselves through the hosted Connect flow — you're read-only there.

A control block is not an HTTP error. The request still returns 200 — the blocked leg comes back inside results[] with status: "failed", a human-readable rejected_reason, and a machine-readable rejected_code of venue_disabled or execution_limit_exceeded. Branch on results[i].rejected_codenot the HTTP status — to detect a control block. (In a multi-leg call, one leg can be control-blocked while others fill.)

Order-constraint rejections

Each leg is also checked against the target venue's order constraints — minimum and maximum size, whole-contract sizing, and the price band — before it reaches the venue. Like a control block, a violation is a per-leg status: "failed" inside a 200 with a machine-readable rejected_code:

  • orders_not_supported — the venue doesn't accept orders.
  • below_min_order / above_max_order — the amount is outside the venue's size bounds.
  • unfillable — on a whole-contract venue, the stake rounds down to zero contracts.
  • price_out_of_band — the price is outside the venue's min/max.
  • above_max_price — the book is there, but priced above the max_price you set. Retry higher, not smaller.
  • insufficient_size — priced within your limit, but the book holds less than this order. Retry smaller, not higher.
  • insufficient_live_depth — nothing at this position at all.
  • The three above used to arrive as a single insufficient_live_depth, which told you “no depth” when the answer was usually “your price is too low”. They are reported separately as of 2026-07-29 — the fix is different in each case.

Read each venue's order_config from Venues & balances to size the order correctly up front and avoid these.

Coming soon — automatic routing

Smart Order Routing is pending regulatory licensing. Routing an order across venues on a customer's behalf — omit partner_id and OpenMarkets picks the best-priced venue, splitting a single leg across partners when that improves the fill — requires an introducing-broker license we're in the process of obtaining. Until it's in place, every leg must name its venue (a liquidity_hash, or a position_hash + partner_id).

When it ships, the only change is that partner_id becomes optional:

Coming soon — best-venue routing
{
  "orders": [
    {
      "position_hash": "ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",
      "amount": 50,
      "max_price": 0.52
      // no partner_id → OpenMarkets routes to the best venue(s)
    }
  ]
}

The response already carries the shape for it — venues[] can list more than one partner, and sor_decision_id records the routing decision — so code you write against the venue-directed API today keeps working unchanged.

Real vs. paper

mode selects the funding source per leg. "real" places on the named connected exchange (money at the venue). "paper" books against the OpenMarkets internal book (the practice wallet, denominated in the ATLAS play currency) so you can exercise the full path — placement, fills, settlement — without real money.

The two currencies are distinct wallets and are never summed:

  • USD — real money (is_real_money: true), held across your connected venues. Real orders book in USD.
  • ATLAS — play money (is_real_money: false), an auto-provisioned practice wallet on the OpenMarkets internal book. Paper orders draw down ATLAS.

Every money surface is partitioned by currency — balances (see Venues & balances), order history (currency on each order), and performance roll-ups. A total that blends the two is always a bug: real P&L and practice P&L are reported in separate buckets.

A paper order books against the internal book, not the venue you named. A leg with mode: "paper" still requires a liquidity_hash (or position_hash + partner_id), but the named venue only anchors the outcome and its reference price — the fill is always placed on the OpenMarkets internal book. So the returned venues[].partner_id will be the internal book, notthe partner you named, and the order reads back with partner.internal_book: true (and currency: "ATLAS"). Always treat the returned venues[].partner_id as authoritative — don't assume the fill landed on the venue you passed in.

List orders

GET /flow/v1/auth/orders returns the acting account's orders, newest first, cursor-paginated. Filter by status, contest, settlement, currency, and more.

bash
curl 'https://api.openmarkets.ai/flow/v1/auth/orders?status=open&limit=50' \
  -H 'Authorization: Bearer eyJhbGc...'

Common query params:

  • status — e.g. open.
  • contest_id — scope to one contest.
  • settled=true · result=<won|lost|...> — only settled orders.
  • currencyUSD (real) or ATLAS (paper).
  • mm=true · model_id · opportunity_type — provenance filters.
  • sort=settled — order by settlement time instead of creation.
  • from · to (ISO-8601) — date range on the active sort column: creation time by default, or settlement time with sort=settled (so ?sort=settled&from=…&to=… is a settled-in-window / calendar-day query). settled_after · settled_before filter settlement time regardless of sort.
  • cursor · limit — pagination (default 50, max 200).

Each item is a router_order (shape below). Use the top-level pagination.next_cursor to page.

Resting orders

GET /flow/v1/auth/orders/resting/me returns just the acting account's working (unfilled / partially-filled) orders — the live book you'd render on a "my open orders" screen. Filled orders drop off automatically.

bash
curl https://api.openmarkets.ai/flow/v1/auth/orders/resting/me \
  -H 'Authorization: Bearer eyJhbGc...'
Response.data
{
  "orders": [
    {
      "router_order_id": "ro_...",
      "title": "Kansas City Chiefs — Moneyline",
      "partner_id": "kalshi",
      "partner_name": "Kalshi",
      "amount": 50,
      "price": 0.52,
      "fill_status": "partially_filled",   // "unfilled" | "partially_filled"
      "filled_amt": 20,
      "unfilled_amt": 30,
      "placed_at": "2026-07-09T18:04:11Z",
      "seconds_since_placed": 42
    }
  ],
  "count": 1
}
For real-time updates instead of polling, subscribe to the orders WebSocket channel — it pushes an order.update the moment a resting order fills, partially fills, or cancels, and an order.settled when it resolves. Poll resting/me on reconnect to re-sync, then resubscribe.

Cancel orders

POST /flow/v1/auth/orders/cancel cancels one or more resting orders by id. The body is a non-empty router_order_ids array.

bash
curl -X POST https://api.openmarkets.ai/flow/v1/auth/orders/cancel \
  -H 'Authorization: Bearer eyJhbGc...' \
  -H 'Content-Type: application/json' \
  -d '{ "router_order_ids": ["ro_abc...", "ro_def..."] }'

There are two distinct failure modes, and they behave very differently:

  • Ownership failure → the whole batch fails with 403 forbidden. Every id is ownership-checked against the acting account first. If even one id isn't yours, nothing is cancelled and you get a 403 — no partial effect.
  • Uncancellable id → a soft, per-id entry inside a 200. An id you own but that can't be cancelled (already closed, or the venue didn't acknowledge the cancel) does not fail the request. It comes back as an entry in skipped[] or errors[] while the rest still cancel.

The 200 response sorts every id into one of three arrays:

Response.data
{
  "cancelled": [
    { "router_order_id": "ro_abc...", "status": "closed", "result": "cancelled" /* full order */ }
  ],
  "skipped": [
    { "router_order_id": "ro_def...", "reason": "Status is 'closed', not cancellable" }
  ],
  "errors": [
    { "router_order_id": "ro_ghi...", "error": "Partner did not acknowledge cancel" }
  ]
}
  • cancelled[] — successfully cancelled at the venue and closed (full order objects).
  • skipped[] — not cancellable to begin with (already closed, not found); each carries a reason.
  • errors[] — the venue cancel failed; each carries an error. These orders are left unchanged so you can retry.
A 200 is only a full success when errors is empty. Always inspect errors[] (and skipped[]) before treating a cancel as complete — the HTTP status alone doesn't tell you every id was cancelled.

Order settlement

GET /flow/v1/auth/orders/:router_order_id/resolution returns the settlement detail for one order: OpenMarkets' own market verdict, the partner's verdict, whether they agree, and the final contest score. Use it to reconcile a settled order and to surface disagreements between our resolution and the venue's.

bash
curl https://api.openmarkets.ai/flow/v1/auth/orders/ro_abc.../resolution \
  -H 'Authorization: Bearer eyJhbGc...'

The order object

Both GET /orders and the resolution endpoint return the full router_order shape:

router_order
{
  "router_order_id": "ro_...",
  "partner_id": "kalshi",
  "partner": { "partner_id": "kalshi", "partner_name": "Kalshi", "logo_url": null, "internal_book": false },
  "currency": "USD",
  "contest_id": "ct_abc123",
  "market_id": "mk_ml",
  "market_side_id": "side_home",

  "title": "Kansas City Chiefs — Moneyline",
  "side": "home",

  "amount": 50,
  "price": 0.52,
  "amount_with_fee": 50.4,
  "fee": 0.4,
  "potential_payout": 96.15,

  "status": "filled",
  "order_type": "fill_or_kill",
  "fill_status": "filled",
  "filled_amt": 50,
  "unfilled_amt": 0,

  "opportunity_type": "value",
  "liquidity_hash": "kalshi:ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",

  "result": "won",
  "winnings": 96.15,
  "net_winnings": 46.15,
  "roi": 0.923,

  "placed_datetime": "2026-07-09T18:04:11Z",
  "settled_datetime": "2026-07-10T02:41:00Z",
  "created_at": "2026-07-09T18:04:10Z"
}

Fields are null until they apply — settlement fields (result, winnings, settled_datetime) populate only after the contest resolves.

Errors

Execution errors use the same structured envelope as the rest of Flow:

HTTPcodeMeaning
400unknown_parameterA field outside the strict /buy contract was sent
400bad_requestInvalid hash, size, or max_price on a leg — incl. a bare position_hash with no partner_id
403account_forbiddenActing on an account the caller doesn't own (act-as ownership)
403forbiddenCancel: a router_order_id not owned by the acting account
404account_not_foundThe acting router_account no longer exists
404order_not_foundNo such order on the acting account
venue_disabled and execution_limit_exceeded are not HTTP errors — a control-blocked leg returns 200 with that code in results[i].rejected_code (see Execution controls above).

End-to-end

The full loop your app runs to place and track a trade:

  • Read a position from /contests/:id/liquidity → pick a venue from its partner_liquidities[] and grab that entry's liquidity_hash.
  • POST /orders/buy with the liquidity_hash (or a position_hash + partner_id), an amount, a max_price, and a fresh Idempotency-Key.
  • Inspect results[] — fill vs. requested, and each router_order_id.
  • Render working orders from /orders/resting/me; cancel via /orders/cancel.
  • After the game, reconcile with /orders?settled=true and /orders/:id/resolution.