API reference

Live trading API

Trade today's trading day: orders, positions, idempotency.

Live trading is paper-trading today's session against the real-time market. You trade an account, not a session: POST /accounts/{id}/orders places, GET /accounts/{id}/positions reads, and the account-level routes resolve the account's own trading day for you. The order, position and transaction shapes are the ones practice sessions use — only the prefix differs (/accounts/{id}/… here, /practice/sessions/{sid}/… for a past day) — plus dry-run and cancel/replace (PUT), which practice does not have. See Live trading for the concepts and Accounts & trading days for the full account API. Requires a registered account.

Your trading day opens itself

There is no call that opens a trading day. Create a live_sim account once; your day then opens by itself on your first order of the day — and a dry-run opens it too:

# create the account (one-time)
curl -s https://api.0dtespx.com/accounts \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Live paper","engine":"live_sim"}'

# there is nothing to call here — the first order opens the day
curl -s https://api.0dtespx.com/accounts/$ACCT/orders \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"type":"limit","price":"5.00","price_effect":"debit",
       "legs":[{"instrument":"SPXW  260507C05950000","quantity":"1","action":"buy to open"}]}'

# which day did I get?
curl -s https://api.0dtespx.com/accounts/$ACCT/days/current -H "Authorization: $TOKEN"

The day opens at the latest tick with the account's carried cash balance (you don't pass a starting capital — that's the account's). Opening it is gated on market hours and fresh market data (400 outside those), on the account being active (409 account_archived), on the engine (409 engine_not_available for a broker account), on today not having settled already (409 day_closed), and on this account having no earlier day still open and unsettled (409 previous_session_unsettled). The whole request body is decoded and validated before any day is opened, so a malformed order is a 400 that leaves nothing behind. Opening the day never waits on the exchange: the day is created by that call alone, the exchange is told about it in the background, and the orders you place next are queued behind that hand-off and processed in order.

Two words are used throughout, and they are not the same:

  • the open day — the account's open trading day. GET /accounts/{id}/days/current returns it, or null when none is open. A placement targets it (and opens today's day when none is open); the date-keyed write forms require one.
  • the active day — the open day, else today's settled day. Every other bare account-level route resolves to it: a read on an account with no day at all answers 200 []; a cancel, replace or liquidation start on a settled day answers 409 day_closed, and with no day at all 404 (orders) or 409 day_not_open (liquidations).

A day settles at the close, and /days/current goes back to null the moment it does. The next order — tomorrow, or whenever you next trade — opens the next day. There is nothing to close, and nothing to re-open.

Rarely, a delayed settlement can leave a day open on an earlier date. While it is, today's day cannot open — a placement answers 409 previous_session_unsettled, because this account's balance isn't final until that day settles. The earlier day stays readable meanwhile through its own date, /accounts/{id}/days/{date}/…, and the cancel path stays available on it — a cancel is never gated on the clock. A price revision there is not: that day's close has passed, so PUT answers 400 market_closed, and a liquidation's own closing orders are refused by the exchange for the same reason. Whatever you do or do not reach, the day resolves every order still working on it at its close. /days/current always names the newest open day.

GET /accounts

Lists your accounts and saves the first one's id as {{account_id}}. Paste a different one into the blocks below if it isn't the live_sim account you trade.

Interactive — run this request from the docs

GET /accounts/{{account_id}}/days/current

The open trading day, or null when none is open. Outside market hours you will see null — that is the honest answer, not an error.

Interactive — run this request from the docs

Every READ on this page is runnable against an account you already own, at any hour: none of them needs a session id to fill in any more, and with no day open they simply answer null or []. The two order blocks are the exception, and both ask you to confirm before they run — including the dry-run, which persists no order but does open your trading day. That is the one call whose outcome depends on the wall clock, and starting a live day by accident from a docs page is not a favour: read the note on each block first.

Trading-day endpoints

The account-level routes are the ones to reach for; the date-keyed forms name a specific day.

Method Path Purpose
POST /accounts/{id}/orders Place an order — opens today's day
POST /accounts/{id}/orders/dry-run Validate + preview — opens today's day too
PUT /accounts/{id}/orders/{orderId} Revise a working limit order's price
DELETE /accounts/{id}/orders/{orderId} Request cancellation of a working order
GET /accounts/{id}/orders · …/orders/{orderId} List / get orders
GET /accounts/{id}/positions Current positions
GET /accounts/{id}/transactions Transaction log
GET /accounts/{id}/history Per-second P&L history (chart line)
POST /accounts/{id}/liquidations Close every open position
GET/DELETE /accounts/{id}/liquidations/current A liquidation's progress / cancel it
GET /accounts/{id}/days Every trading day, newest first
GET /accounts/{id}/days/current The open day, or null
GET /accounts/{id}/days/{date} One day by YYYY-MM-DD
GET /accounts/{id}/days/{date}/{orders,positions,transactions,history} The same reads, on one day
PUT /accounts/{id}/days/{date}/orders/{orderId} Revise, on one day
DELETE /accounts/{id}/days/{date}/orders/{orderId} Cancel, on one day
POST /accounts/{id}/days/{date}/liquidations Close every open position, on one day
GET/DELETE /accounts/{id}/days/{date}/liquidations/current Progress / cancel, on one day

The date-keyed writes exist for one case: an older day still open behind a delayed settlement. They require that day to be open — a settled one answers 409 day_closed, a date you never traded 404 day_not_found. There is deliberately no date-keyed POST …/orders: a placement always opens or targets today.

Place an order

The request body matches the orders API. On a live account, two additions make retries safe:

curl -s https://api.0dtespx.com/accounts/$ACCT/orders \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: ord-2025-05-07-001' \
  -d '{
    "type":"limit","price":"5.00","price_effect":"debit",
    "legs":[{"instrument":"SPXW  260507C05950000","quantity":"1","action":"buy to open"}]
  }'

POST /accounts/{{account_id}}/orders/dry-run

Runs every validation and returns the same response WITHOUT persisting an order — but it OPENS today's trading day on this account, which is why it asks before it runs. Point the leg at TODAY's SPXW expiry and a strike that exists, or it fails validation.

Interactive — run this request from the docs

POST /accounts/{{account_id}}/orders

This places a REAL order on this account's trading day — it opens the day if none is open, and it can fill against the live market. Dry-run it first, and edit the leg to today's expiry. The Idempotency-Key is held across a 504 or 409 so a retry can't double-place.

Interactive — run this request from the docs

  • Idempotency-Key (header, optional but recommended) — scoped per trading day. A duplicate submission within the retention window returns the original response instead of placing a second order; a duplicate of an order that is still queued returns 202 with that order as it stands. The same key under a different account or day mints a distinct order. This is also how you resolve a 504 outcome_unknown: retry under the same key until it returns a terminal outcome (a 409 duplicate_idempotency_key while a five-second request is still in flight means keep retrying).
  • client_order_id (body, optional UUID) — used as the order's id; a replay with the same id resolves to the existing order. When omitted, one is derived from the idempotency key, so most clients never need to set it.

Live orders enforce the same trading rules as practice, at the exchange:

  • 0DTE SPX index options only. Every leg must be a 0DTE cash-settled SPX index option; an equity, ETF, non-SPX, or non-0DTE leg is rejected with 400 only 0DTE SPX index options can be traded (and the more specific only SPX index options can be traded … / only 0DTE options …).
  • Defined-risk only — no naked shorts. An order whose resulting position would hold an uncovered short option is rejected with 400 naked short positions are not allowed, regardless of capital, checked before the buying-power gate. Cover the short with a long of the same type to make it a spread. Unlike practice rejections (JSON {"message": …}), the exchange relays live rejection text as a plain-text body — the message string is the same.

Order lifecycle

Live orders never time-travel. status is the persisted state, it only moves forward along one ladder, and it stays terminal once set:

pending → routed → live → {filled, canceled, expired, rejected}

Status Meaning
pending We accepted the order; nothing downstream has confirmed it yet. Usually a fraction of a second, but it lasts until the exchange processes the order — the order is placed throughout
routed Confirmed by the next hop, not on a book yet. Reserved — nothing emits it today; handle it now and it costs you nothing later
live Resting on the book, awaiting a matching tick
filled Executed
canceled Your cancel reached the book while the order was still resting
expired Auto-expired at the 4:00 PM ET close
rejected Failed a buying-power recheck at fill time, or never reached the book (rejection_reason explains)

A rung can be skipped — an order can go from pending straight to a terminal — but the sequence never runs backward, and the first terminal recorded is final.

The three non-terminal states are all working: they count toward your open orders, and both a cancel and a replace are accepted on any of them. Buying power is reserved once the order rests on the book — an order still pending is accepted but unconfirmed, so it reserves nothing until it is, however long that takes. It is checked when the exchange processes the order, against the balance at that moment.

The POST response normally carries live for a resting order, because the API waits up to five seconds for the exchange. When the exchange hasn't confirmed a limit or a stop in that time, the answer is 202 with the order pending: it is placed, it stays queued for the exchange, and it is confirmed when the exchange processes it — at the latest the close resolves it, stamping an unprocessed order rejected with not_processed_before_close. Either way pending is what you see on the order_update stream, which emits the order at admission and again at acceptance — that is where the progression is visible. A market order is the one placement that keeps the five-second promise instead: it is worth nothing minutes later, so an exchange that can't process it in time leaves it rejected (deadline_exceeded) and answers 503.

A stop the exchange doesn't take up inside the answer window is measured against the market of the second it is processed, which isn't the second you sent it: a stop whose trigger the market has already reached by then is refused (stop_already_triggered) rather than fired late. A stop the exchange picks up promptly is unaffected — it rests, and triggers on a later second.

cancel_requested: true is a separate flag, not a status: see Cancel and replace.

No orders are accepted after the close (400 market_closed).

Cancel and replace

# request cancellation of a working order
curl -s -X DELETE https://api.0dtespx.com/accounts/$ACCT/orders/$ORDER_ID -H "Authorization: $TOKEN"

# revise a working LIMIT order's price (cancel + replace, atomically)
curl -s -X PUT https://api.0dtespx.com/accounts/$ACCT/orders/$ORDER_ID \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"price":"5.50","price_effect":"debit"}'

Both resolve to the account's active day — the open day, else today's settled one: once today's day has settled they answer 409 day_closed, and only an account with no day at all answers 404. To reach an order on an older day that is still open, name the date — …/accounts/{id}/days/{date}/orders/{orderId}.

Cancel is a request, and best-effort. It is accepted in every non-terminal state — flattening risk must never depend on where the order sits on the ladder, or on the clock. 409 comes back in two cases: the order is already terminal, or the Idempotency-Key already belongs to a different order (idempotency_key_reuse). The 204 means the cancel was accepted and queued behind whatever is already in flight for that order, not that the order is gone. The order then carries cancel_requested: true (show it as "canceling…") until it resolves one of four ways: it was resting → canceled; it was still queued and the exchange hadn't taken it up → canceled at once, and the exchange never sees the placement; it was already with the exchange and never reached the book → rejected (deadline_exceeded for a market order, not_processed_before_close at the close) and the cancel is a no-op; it filled firstfilled, with cancel_requested still true. That last case is irreducible — any exchange can match in the moment before a cancel lands — so read the terminal status, not the 204, as the outcome. A cancel the exchange hasn't confirmed inside the five-second wait answers 202 with the order as it stands, and keeps travelling. Cancelling an order whose revisions are still queued cancels the whole chain — nothing of it is left working.

A replace is a revision, and the revision is its own order. Only price and price_effect may change; type, legs, and underlying are fixed. It is accepted against any working order — pending, routed, or live — because the revision is recorded the moment the call is accepted (a pending order whose replaces_order_id names the one you are revising) and applied when the exchange gets to it, in the single transaction that cancels the original. The 200 is the revision, under its own id; both rows stay in your order list, linked.

That linked pair is also how the call resolves when the exchange doesn't answer inside the wait: the PUT answers 202 with the revision pending, and the revision stays queued. A revision that is never applied ends in the list as a canceled row (you cancelled it) or a rejected one (the close resolved it), while the order it was revising keeps working — so read the rows back rather than the HTTP status. Retrying under the same Idempotency-Key is safe: a retry of a still-queued replace answers 202 with the same revision.

The 409s: order is no longer working (terminal), order is being canceled (cancel_requested already set), a replace is already pending for this order (one working revision at a time — revise the revision, or cancel it), and, on a reused key, idempotency_key_reuse (that key belongs to a different order) or replace_attempt_resolved (the revision that key minted has already resolved — use a new key). Replacing a non-limit order returns 400.

Read your state

curl -s https://api.0dtespx.com/accounts/$ACCT/days/current    -H "Authorization: $TOKEN"  # the open day, or null
curl -s https://api.0dtespx.com/accounts/$ACCT/orders          -H "Authorization: $TOKEN"  # orders
curl -s https://api.0dtespx.com/accounts/$ACCT/positions       -H "Authorization: $TOKEN"  # positions
curl -s https://api.0dtespx.com/accounts/$ACCT/history         -H "Authorization: $TOKEN"  # per-second P&L history
curl -s https://api.0dtespx.com/accounts/$ACCT/transactions    -H "Authorization: $TOKEN"  # transactions

Every one of those resolves to the active day — the open day while you are trading, today's settled day after the close, and [] on an account with no day at all. For any other day, put its date in the path: /accounts/$ACCT/days/2026-05-07/positions, and GET /accounts/$ACCT/days lists what there is.

GET /accounts/{{account_id}}/orders

Every order of the account's active day, with its persisted lifecycle status. An account with no day answers [].

Interactive — run this request from the docs

GET /accounts/{{account_id}}/positions

The active day's open positions, marked against the latest processed tick — the ?at= cursor is ignored while the day is open.

Interactive — run this request from the docs

GET /accounts/{{account_id}}/transactions

The day's fills, plus the settlement entries once the close has been processed.

Interactive — run this request from the docs

GET /accounts/{{account_id}}/history

The per-second P&L line behind the chart. Switch the interval on to down-sample a full day into something readable.

Interactive — run this request from the docs

GET /accounts/{{account_id}}/days

Every trading day this account has had, newest first — the settled archive plus the open day, if one is open.

Interactive — run this request from the docs

Live state also pushes over the WebSocket — subscribe session_events with your account_id, before the first order, and the server enrolls whatever day you open. REST is authoritative for an operation you just performed; the WebSocket is the source of truth for changes you didn't initiate (other tabs, fills, the close). De-dupe orders by id and prefer the newer updated_at. After a disconnect, refetch positions/orders/transactions to recover.

Settlement and ending

Settlement runs automatically once the closing tick arrives; the day's status becomes settled, its closing balance carries into the account's cash_balance, and GET /accounts/{id}/days/current returns null. The account-level reads keep answering — they fall back to today's settled day — and /accounts/{id}/days/{date}/… is how you read it once tomorrow's day is the current one. A trading day can't be stopped: it runs to the close and settles there.

Error semantics

Status Meaning
202 Accepted and queued — the order (or the replace, or the cancel) is recorded and the exchange hadn't confirmed it inside the five-second wait. The body is the order as we hold it: pending for a placement or a revision, cancel_requested: true for a cancel. Watch order_update for the rest.
400 Validation error — market closed or live data unavailable (the day cannot be opened), non-0DTE-SPX leg, naked short, insufficient quantity to close, or insufficient buying power. Rejections relayed from the exchange are plain-text bodies; validation the API catches first keeps the JSON envelope — handle both.
404 No such account, or no order by that id on the day the route resolved to. Two day-level cases land here as well: day_not_found (a …/days/{date}/… route naming a date this account never traded), and a cancel or revise on an account with no trading day at all — there is no order to name where there is no day. (A placement instead OPENS the day, and a liquidation answers 409 day_not_open.)
409 The day or the order forbids the operation. Day-level: day_closed (the day the route resolved to has settled — including today's, minutes after the close), day_not_open (a liquidation on an account with no open day — a liquidation never opens one), account_archived, engine_not_available, previous_session_unsettled (an earlier day of this account is still open and unsettled, so today cannot start). Order-level: already terminal, already being canceled, or (replace only) already carrying a working revision. An Idempotency-Key adds duplicate_idempotency_key (the original is still in flight — keep retrying the same key), idempotency_key_reuse, and replace_attempt_resolved.
503 The exchange could not process the request in time — always safe to retry, under the same key. It is a market-order answer (and applies to any order write in the last five seconds before the close — and, for a cancel, at any time after it, since a cancel stays accepted past the close but has no day left to ride to); everything else is queued and answered 202 instead. Not a promise that nothing was applied: the order list is the truth, and a market order the exchange never confirmed is stamped rejected there.
504 outcome_unknown — the exchange couldn't confirm within the deadline whether the request applied, so it may or may not have. Resolve it: retry under the same Idempotency-Key (the retry returns the original outcome — a 409 while it's still in flight means keep retrying), or reconcile with a GET on the order or the day.