API reference

Orders API

Place, preview, and cancel orders, including multi-leg.

Orders open and close positions in one trading day. The endpoints have the same shape on both surfaces — an account-less practice session (/practice/sessions/{sid}/orders) and a live account (/accounts/{id}/orders, which resolves the account's own trading day) — so the request/response bodies below apply to both; only the path prefix differs (a live account additionally offers dry-run and PUT replace, which practice does not). This page covers placing, previewing, inspecting, replacing, and canceling them. For the concepts — order types, fill rules, SPX ticks — see Orders & order types.

Endpoints

Method Path Purpose
GET /accounts/{id}/orders List orders
POST /accounts/{id}/orders Place an order
POST /accounts/{id}/orders/dry-run Validate + preview without persisting
GET /accounts/{id}/orders/{orderId} Get one order
PUT /accounts/{id}/orders/{orderId} Revise a working limit order's price
DELETE /accounts/{id}/orders/{orderId} Cancel/delete an order

The live routes name no day: they resolve the account's active day (the open day, else today's settled day — a cancel or replace on a settled day answers 409 day_closed), and a placement opens today's day if none is open (live trading). The date-keyed forms /accounts/{id}/days/{date}/orders… name one day outright, which is how you reach an order on a day still open behind a delayed settlement.

The runnable blocks on this page use the practice prefix, because a practice day can be opened at any hour and thrown away afterwards — the request and response bodies are the ones documented below either way. Start by opening a day and moving its clock past data-start-time; the session id is then reused by every block that follows.

POST /practice/sessions

Opens a throwaway $100,000 practice day. Any past trading date works — take one from GET /market-data/sessions if this one isn't in the archive.

Interactive — run this request from the docs

PATCH /practice/sessions/{{session_id}}

Sets the session's replay clock to the given time. Orders are refused before the day's data-start-time, so park it a few minutes into the session; keep the date in step with the session you opened.

Interactive — run this request from the docs

Instrument strings

Legs name instruments by string:

Type Format Example
Equity <SYMBOL> SPX
Equity option <ROOT><YYMMDD><C|P><strike×1000, 8-digit> (21-char OSI) SPXW 250115C05950000

The equity-option string is canonical OPRA/OSI: a 6-char root left-justified and space-padded, the YYMMDD expiry date, a C or P side letter, and the strike × 1000 zero-padded to 8 digits. SPX 0DTE options use the SPXW weekly root (so SPXW followed by two spaces). An option is identified by root + date + side + strike; the session-close time is implied, not encoded.

Place an order

A limit debit call vertical (buy the 5950 call, sell the 5970 call) — multi-leg orders must be limit:

curl -s https://api.0dtespx.com/accounts/$ACCT/orders \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "type": "limit",
    "price": "10.50",
    "price_effect": "debit",
    "legs": [
      {"instrument":"SPXW  250115C05950000","quantity":"1","action":"buy to open"},
      {"instrument":"SPXW  250115C05970000","quantity":"1","action":"sell to open"}
    ]
  }'

POST /practice/sessions/{{session_id}}/orders

Places the vertical in your practice day. The legs' YYMMDD has to match the session date (0DTE only) and the strikes have to exist on it, or the order is rejected with 400.

Interactive — run this request from the docs

The response carries status, fees, and a buying-power-effect block projecting the impact on margin and available buying power; once the order is filled it also carries fill_price, fill_datetime, fill_price_effect and the per-leg transactions — and, on a live or bot session, fill_quotes, the market quotes at the time of the fill:

{
  "id": "a7e2c9d1-…",
  "type": "limit",
  "price": "10.50",
  "price_effect": "debit",
  "status": "filled",
  "fill_price": "10.50",
  "fill_price_effect": "debit",
  "slippage": "0.05",
  "execution_price": "10.45",
  "fees": "3.44",
  "legs": [  ],
  "transactions": [  ],
  "buying-power-effect": {
    "change-in-buying-power": "1053.44",
    "change-in-buying-power-effect": "Debit",
    "new-buying-power": "98946.56"
  }
}

A single-leg market order fills immediately at the natural price (ask for a buy, bid for a sell); market and stop orders must be single-leg.

Limit and stop orders

How a limit or stop order fills — two rules, identical in practice, live, and backtests:

  • A price past the market fills at once; anything else takes 3 consecutive seconds. The order is checked once a second. A price strictly past the market — better than the mid-based execution price, not merely level with it — fills on the very check that sees the crossing, with no waiting period: on the second you placed it, if it already reached past the market then, or on the first second the market moves through it afterwards. Everything else has to have its condition hold on three checks in a row, so the earliest it can fill is 2 seconds after placement. (When the platform cannot price the second you placed on, a crossing order rests instead and then fills at its own price on the first check that sees the market through it — staleness can delay or downgrade such a fill, never manufacture one.) So a one-second flicker to your price fills nothing and a second the market can't be priced at breaks the run and starts it over, but a one-second move through your price does fill.
  • A limit the market came to fills at its own price. Not at the mid it touched — fill_price equals the price you asked for (per unit of the structure). That holds both ways such an order fills: after its three checks at the level, and on the second the market moves through it while it is working (a resting order trades at its limit, and the incoming side keeps the improvement). An order priced at the mid is resting too: it asked for the mid, it did not reach past it. Three cases fill marketable instead, at the mid-based execution price shifted by your slippage and bounded at the natural — never above the net ask for a debit, never below the net bid for a credit: a market order, a triggered stop, and a limit whose price was already strictly past the market at the second that price was set — that one crossed the spread on purpose, so it gets the price the market was showing at that second, which is strictly better than the price it asked for.

"The second that price was set" is not always the second you placed the order. Giving an order a new price starts it over — a PUT replace, every step of a smart liquidation's re-pricing ladder, and every step of the automatic re-pricing the platform runs on a strategy's own orders, in a bot session and in a strategy's results alike, each count as a fresh placement here: the new price is tested against the market at the second it takes effect, and the three-second run restarts from there. So a re-priced order that steps through the market crosses the spread at that step: it fills right there, and comes back with a fill_price better than its own price even though you never placed it that way.

In the example above, price is 10.50 and slippage is $0.05, so execution_price — the mid the order had to reach — is 10.45. If the mid was at or above 10.45 when the order went in, the order rested, and once the mid sat at or below 10.45 for three seconds it filled at the 10.50 it asked for. If the mid was already below 10.45, the order crossed the spread and the POST came straight back filled, at the mid-based price as it stood at that second — shifted by slippage, but never past the natural.

There is one more marketable case, and you don't place it by hand: an automatic stop-loss close — the closing order a strategy or the Strategy Builder generates to cap a loss. If the market runs through that order's price and stays through it for three consecutive seconds, it fills at the market instead of resting at a price the market has left behind. Without it a stop-loss on a runaway move would sit unfilled exactly when it is needed.

Add price for a limit and stop_trigger for a stop, plus a price_effect of debit or credit. Slippage is not a request field — it is a single per-account setting (in Settings → Simulator costs, defaulting to $0.05 on new accounts) that the server applies to the fill condition, and is echoed back on the order's slippage/execution_price — see Slippage:

{"type":"limit","price":"1.50","price_effect":"credit","legs":[  ]}
{"type":"stop","stop_trigger":"12.00","price_effect":"debit","legs":[ /* single leg */ ]}

A limit whose price reaches strictly past the market comes back filled — it crossed the spread on arrival — with fill_price, fill_datetime, and, on a live or bot session, fill_quotes. On a live or bot session that fill runs inside the call at the exchange, so the call can also fail the way a market order's can there: 400 execution_failed when the fill cannot be executed against the market, and 400 no_fill when nothing filled against it; neither leaves an order working. (503 market_data_error belongs to market orders, which read the market inside the call — a crossing limit has already read it to decide that it crosses, so a market it cannot read simply leaves the order resting.) A practice session has none of them — it pre-computes the fill from historical data, so a market it cannot read is already the 400 of validation step 5 below. Every other limit, and every stop, comes back live — resting on the book. On a practice session its fill_datetime is pre-computed; advance the session clock past that moment and it shows filled. On a live session it stays live until three matching ticks arrive in a row, or until the market moves through it. On a practice session a stop whose trigger is already met is rejected with 400; on a live session it rests, and the exchange refuses it (stop_already_triggered) only when it is a queued stop the exchange didn't process inside the answer window — see Live trading.

The market at the fill

An order filled on a live or bot session also carries fill_quotes: the market quotes at the time of the fill, per leg, plus the order's net bid and ask at that moment. It is there so you can see the market your order filled in without reconstructing it afterwards.

{
	"id": "a7e2c9d1-…",
	"status": "filled",
	"fill_price": "10.50",
	"fill_price_effect": "debit",
	"fill_datetime": "2026-08-06T14:30:05Z",
	"fill_quotes": {
		"quotes": {
			"SPXW  260806C05950000": { "bid": "12.30", "ask": "12.60" },
			"SPXW  260806C05970000": { "bid": "1.90", "ask": "2.15" }
		},
		"quotes_at": {
			"SPXW  260806C05950000": "2026-08-06T14:30:04Z",
			"SPXW  260806C05970000": "2026-08-06T14:30:05Z"
		},
		"net_bid": "10.15",
		"net_ask": "10.70"
	}
}
  • quotes — each leg's bid and ask, keyed by that leg's option symbol. Every leg of the order is present: the snapshot is all-or-nothing, so a partial one is never returned. Every price is a decimal string.
  • quotes_at — the time of the quote used for each leg, keyed by the same symbols. This is the quote's own timestamp, not the fill second. A leg takes the closest quote within a second of the fill, so on an order the market came to a value here can sit up to one second on either side of fill_datetime — trailing it or leading it. A market order is a wider case: it fills on the spot against the latest quotes available while its fill is stamped with the moment you placed it, so its timestamps can trail fill_datetime by a few seconds. A limit that crossed the spread at placement is not that case: it is priced off the placement second's own market. Read quotes_at; never assume the fill second. It is the same field the bot decision log reports on its pricing entries.
  • net_bid / net_ask — the order's net bid and ask at that moment, per unit: the leg-quantity greatest common divisor is divided out (a 2×/2× vertical reports the same numbers a 1×/1× does), and the credit/debit direction comes from the order's price_effect / fill_price_effect, never from a sign here. That is also why, on a ratio spread, you cannot re-derive them by adding up quotes. Do not assume net_bid is positive: on a near-zero or very wide combo the natural bid side crosses through zero, so it can come back as 0 or as a negative number — a wide debit vertical can report a net_bid of -0.25 against a net_ask of 0.55. The only ordering guarantee is net_bidnet_ask.

These are the quotes at the fill, not the inputs to the fill price. fill_price follows the rules above, and in most cases it is not arithmetic on this snapshot: a limit the market came to fills at its own price, and a limit priced past the market books the market it crossed at the second that price was set. So fill_price can sit outside net_bid/net_ask — the pair does not bracket it, and none of these numbers is an execution-quality or slippage readout. There is no midpoint field either: the reference price a limit or stop is measured against comes from the tick rules above, not from averaging the two bounds.

The whole object is absent — never null — when there is nothing to report: on any status other than filled, on an order that filled before this field shipped, and on the rare fill where a leg's quote could not be read. It is also always absent on a practice order, which is priced against historical data and never captures it; that is why the runnable blocks on this page never show it. Live and bot orders carry it everywhere they are returned — the REST reads, the POST / PUT / dry-run bodies, and the order_update event.

Validation order

Independently of the checks below, order placement requires the session clock to be at or after the day's data-start-time (the moment historical data begins, from GET /market-data/sessions); an order placed while the clock is earlier is rejected with 400 (trading is available from <time>). The clock itself can be moved anywhere in the session — only order placement is gated.

These checks run in this order, each with its own 400:

  1. Order type and leg shape parse; market and stop orders must be single-leg. Every leg must be a 0DTE SPX index option (only 0DTE SPX index options can be traded).
  2. price required for limit, stop_trigger for stop, price_effect for both. (Slippage is taken from your account setting, not the request body.)
  3. SPX option tick rules — single-leg < $3 → $0.05, ≥ $3 → $0.10, multi-leg → $0.05.
  4. A limit price can't exceed the structural maximum profit of the legs.
  5. Market data must exist for every leg at the current time.
  6. A stop can't already be triggered. (Practice sessions only. On a live session a stop that is already through its trigger rests and fires on a later second, exactly as it does in a backtest; the live exchange refuses it only when it is a queued stop it did not process inside the answer window — stop_already_triggered.)
  7. Available-to-close: a to close leg can't exceed the quantity you hold (insufficient quantity to close).
  8. Defined-risk only: the resulting position can't hold an uncovered short option (naked short positions are not allowed) — checked before buying power, so a naked-adding order returns this, not a buying-power error, regardless of capital.
  9. Buying-power check, including all existing working orders.

Rejections are JSON {"message": …} on practice sessions. The naked-short and available-to-close checks read your resulting book (current filled + working positions plus the new order's legs), so they also block stripping the long leg out of a spread you already hold.

Dry-run

POST /accounts/{id}/orders/dry-run runs every validation and returns the same response (minus transactions) without persisting anything — no order, no trades, no history change. It returns 200 instead of 201. Use it for pre-trade confirmation; a successful dry-run strongly implies the real POST will succeed too. (On a live account the result is advisory — the market may move between the dry-run and the real place. It also opens today's trading day, exactly as a real order does: the preview is priced by the exchange, so the day has to exist first.)

A dry run reports the same verdict the real placement would. A limit whose price reaches strictly past the market previews as filled — and, since the preview fills it at the exchange exactly as a real placement would, it can surface that path's 400 execution_failed / 400 no_fill — while a limit merely level with the market previews as live, as does every stop. A market order previews as filled.

Order status and fees

GET /practice/sessions/{{session_id}}/orders

Every order in the day, with each status computed against the current clock.

Interactive — run this request from the docs

GET /practice/sessions/{{session_id}}/orders/{{order_id}}

The single order you just placed — rewind the clock with PATCH and this same read flips it back to live.

Interactive — run this request from the docs

On a historical session, order status is computed from fill_datetime versus the session's current clock time — filled if the fill is at or before now, otherwise live; rewind the clock and a filled order can revert to live. On a live trading day, status is the persisted lifecycle state and never time-travels. Fees follow suit: a filled order reports the actual summed fees; an unfilled one shows a schedule-based estimate. A filled order's fees follow the account effect — the part of a leg that reduces a position you already hold is charged the closing rate, the part that adds new exposure the opening rate, whatever the leg's action is worded as. The estimate on an unfilled order is computed without that context, so it prices each leg by its stated action: a to close leg is quoted at the closing rate. The two land on the same number for any order whose legs are labelled the way they will act, and part company only where they are not. See Fees.

A live order walks one ladder, always forward, and may skip a rung:

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, and it lasts until the exchange processes the order — which is placed the whole time.
routed The next hop confirmed receipt, but the order is not on a book yet. Reserved — nothing emits it today; handle it so it costs you nothing when it starts occurring.
live Resting on the book: fillable. Code your working-order view against this one.
filled canceled expired rejected Terminal. The first terminal recorded wins; nothing overwrites it.

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 has been accepted and not yet confirmed, 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. accepted_seq — the per-session book ordinal, carried on the WebSocket order_update payload — is null for as long as the order is pending or routed, and is stamped the moment the order is sequenced. That is normally the move to live; an order refused in the same instant it was sequenced goes straight to rejected carrying its ordinal, so a terminal order with an accepted_seq did not necessarily rest on the book.

One more field is not a status at all: cancel_requested is present, and true, from the moment a cancel is accepted for the order until the order resolves. It rides any status, terminal ones included. See Cancel.

Two more keys appear only on an order a strategy placed: trade_id and trade_label name the trade the order belongs to, matching the rows a session's trades read returns. They are present from the order's first order_update frame onward, and absent on every order you place yourself — group those by the trades read's own order_ids instead.

Replace

curl -s -X PUT https://api.0dtespx.com/accounts/$ACCT/orders/$ORDER \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"price":"10.40","price_effect":"debit"}'

Only price and price_effect may change — type, legs, and underlying are fixed.

On a live trading day the revision is an order of its own. It exists from the moment the call is accepted, with replaces_order_id naming the order in the path, and the exchange then cancels that order and puts the revision on the book in one transaction — so the book never holds two resting copies, the order named in the path goes terminal, and the 200 comes back as the revision, under its new id.

Both orders stay in your order list, linked by that field, and that is where a replace resolves. The revision ends live (or filled) if it was applied. Until then it is an ordinary pending order — placed, cancellable, and answered 202 instead of 200 when the exchange hasn't confirmed the replace inside the five-second wait. If the replace is never applied, the revision ends canceled (you cancelled it) or rejected (the close resolved it, not_processed_before_close), and the order it was revising is still working. So neither the 202 nor a 503 is proof that nothing happened: read the two rows back.

A replace is accepted against any working orderpending, routed, or live. One that has not reached the book yet still has an intent to revise, and the revision is applied when it is acknowledged. Replacing a non-limit order returns 400; these return 409:

Cause What comes back
It is already terminal. JSON: {"message": "order is no longer working"}
A cancel was already accepted for it (cancel_requested). JSON: {"message": "order is being canceled"}
It already has a working revision. Revise that one, or cancel it. JSON: {"message": "a replace is already pending for this order"}
That Idempotency-Key was already used for a different order. Use a new key. JSON with a code: {"error": "idempotency_key_reuse", "message": "…"}
The original request under that key is still in flight. Keep retrying it. Plain text: duplicate idempotency key, prior request still in flight
The revision that key already minted has resolved. Use a new key. Plain text: this replace attempt already resolved; retry with a new Idempotency-Key

Only the fourth row carries a machine-readable error code; the rest give you a message and the status. The plain-text bodies come from the exchange, and the first two causes reach you that way too when the order changes state while your call is in flight — same 409, the exchange's own wording (order already filled, order is being canceled), plain text rather than JSON. So match on the status, not on the body, and treat the body as text you may show.

Revising a revision is allowed: a working revision is a working limit order like any other, so a chase can walk a price down A → B → C. What is refused is a second revision of the same order while the first is still working.

Cancel

curl -s -X DELETE https://api.0dtespx.com/accounts/$ACCT/orders/$ORDER -H "Authorization: $TOKEN"

DELETE /practice/sessions/{{session_id}}/orders/{{order_id}}

Deletes the order captured above, along with its trades, and recomputes the day's history. Practice only — nothing here touches a live account.

Interactive — run this request from the docs

Returns 204. On a practice session, deletion removes the order and its trades (including any settlement trades it affected) and recomputes history — it runs against historical data, so it takes effect at once.

On a live session a cancel is a request, and best-effort. It is accepted in every non-terminal state (pending, routed, live), because flattening risk must never be blocked by where the order sits on the ladder — or by the clock, so it is accepted after the close too. 409 comes back in two cases: the order is already terminal, or the Idempotency-Key you sent 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. From that moment the order reads cancel_requested: true (show it as "canceling…"), and one of four things follows:

  1. The order was resting → it goes canceled.
  2. The order was still queued and the exchange hadn't taken it up → the cancel resolves it canceled at once, and the exchange never sees the placement. If queued revisions of it exist, the whole chain is cancelled with it.
  3. The order was already with the exchange and never reached the book → it ends rejected (deadline_exceeded for a market order, not_processed_before_close at the close), cancel_requested still true, and the cancel is a no-op — there was nothing on a book to take off it.
  4. The order fills first → it ends filled, with cancel_requested still true.

Case 4 is irreducible: any exchange can match in the moment before a cancel lands, which is why every broker treats cancels as best-effort. Take the terminal status, not the 204, as the outcome — read the order back, or watch the day's order_update events.

A cancel the exchange hasn't confirmed inside the five-second wait answers 202 with the order as it stands (cancel_requested: true) and keeps travelling. Replaying the same Idempotency-Key answers the same way — 204 once it has resolved, 202 while it is still queued — and using that key for a different order returns 409 idempotency_key_reuse.

Liquidations

A liquidation closes every open position on one trading day in one call — the API behind the app's Close all positions button. You pick a method, the server derives the closing orders from what you currently hold, and it works them until the book is flat, the day ends, or you cancel. On a live account it targets the open day: with none open it answers 409 day_not_open, and it never opens one.

Method Path Purpose
POST /accounts/{id}/liquidations Start a liquidation on a live account's open day
GET /accounts/{id}/liquidations/current Progress, or the last one's summary
DELETE /accounts/{id}/liquidations/current Cancel a running liquidation
POST /practice/sessions/{sid}/liquidations Run one on a practice session — synchronous
curl -s https://api.0dtespx.com/accounts/$ACCT/liquidations \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"method":"smart"}'

The two methods

Both surfaces take the same body — {"method": "smart"} or {"method": "aggressive"}.

smart is priced to capture the best available exit. Your positions are grouped into multi-leg limit orders of up to four legs, each placed at a favorable price, and every working order is re-priced every 5 seconds — one tick at a time toward the market, never back away from it — until it fills. It is slower than a market order, but you keep the spread.

aggressive closes each position on its own with a market order, in two rounds: every short is bought back first, then the longs those closes release. It is unconditional and immediate, but you take whatever the market offers, which on a wide 0DTE spread can be materially worse than the mid. One exception: a leg with no usable natural price (nothing bid when selling, nothing offered when buying) gets a one-tick ($0.05) limit order instead — a market order there would print at zero — so that leg may rest instead of filling straight away.

Grouping keeps you covered

Closing orders are grouped so that no partial set of fills can ever leave you holding an uncovered short. Inside a single order, the long quantity being closed never exceeds the short quantity being closed in the same expiration and option right, and long-only closes are capped at the longs you hold in excess of your shorts. That invariant is also why aggressive buys the shorts back before it sells the longs. Whatever subset of the closing orders fills first, your book is never more naked than it started — so a liquidation is never rejected by the defined-risk rule.

Starting one cancels your working orders

A liquidation first cancels every working order on the day — including stop-losses and take-profits — and they are not restored. Not when it completes, not when it ends partially, and not when you cancel it. Re-place any protective orders you still want afterwards.

While one is running on a live trading day, placing (POST) or replacing (PUT) an order on that day returns 409 liquidation_in_progress. Cancelling an order (DELETE …/orders/{orderId}) stays available — you can always reduce risk by hand.

The liquidation snapshot

All four endpoints answer with the same object:

{
	"method": "smart",
	"status": "running",
	"started_at": "2026-07-17T14:31:02Z",
	"orders": [
		{
			"order_id": "9f3c1a24-…",
			"status": "live",
			"legs_summary": "buy to close 1 SPXW  250115P05900000 + sell to close 1 SPXW  250115P05880000",
			"current_price": "1.35"
		}
	],
	"positions_remaining": 2,
	"errors": []
}
status Meaning
running still working — live sessions only
completed the book is flat
partial it ended with positions still open — a session that closed mid-liquidation, or a group whose price never came
canceled you cancelled it, and its orders were cancelled with it
failed it stopped on an internal error

orders[].order_id is each closing order's current id — re-pricing replaces an order, so the id moves as it walks; each one is an ordinary order under GET …/orders in the meantime. status on an order is its real lifecycle state (pending, routed, live, filled, canceled, expired, rejected), plus two extras: stalled for a live order the workflow parked after the exchange rejected it, and unfilled for a practice group that reached the close without filling (no order_id — nothing was recorded for it). positions_remaining is how many positions were still open at the last look; positions_unresolved (present only when non-zero) counts longs the aggressive method could not sell because the shorts covering them never bought back. errors is an array of deduplicated messages — a workflow can carry errors and still finish completed.

Live: start, poll, cancel

POST returns 202 with the initial snapshot; the workflow then runs server-side and you follow it with GET …/liquidations/current (polling every couple of seconds is plenty). A finished liquidation's summary stays readable for about 15 minutes, or until a new one starts on that day — after that the GET returns 404 no_liquidation.

DELETE …/liquidations/current stops it and cleans up after it: nothing further is placed or re-priced, everything it has working is cancelled, and the 200 only comes back once those cancellations are terminal — so by the time you hold the final snapshot the book has stopped moving. Positions it already closed stay closed.

A liquidation is live progress, not stored state. In the rare case one is interrupted, the closing orders it already placed simply keep working as ordinary orders — visible under GET …/orders, cancelable, and re-running the liquidation adopts whatever is still open.

Practice: one synchronous call

POST /practice/sessions/{sid}/liquidations runs the same close plan through the day's historical data, forward from the session's replay clock, and answers 200 with the finished summary. There is nothing to poll and nothing to cancel. Your slippage setting governs how quickly its limit orders fill — a wider setting needs the market to move further before a close prints, exactly as for an order you place by hand.

The cancellations and the closing orders are applied atomically — if anything fails, nothing changes at all and your working orders are untouched — and your account statistics are recomputed immediately afterwards. Every timestamp in the summary is a session time, and the orders it records carry the session times they were placed and filled at — which can be after the current clock. Advance the clock to watch them fill.

A group that never reached its price before the close is not recorded: it comes back in orders with status unfilled and no order_id, its positions stay open, and the summary's status is partial. Running the liquidation again is the retry.

Liquidation errors

Response When
409 liquidation_in_progress one is already running on that day — also what order place/replace returns while it runs
409 no_open_positions live: the book is flat, so there is nothing to close
409 no_liquidatable_positions practice: every open position is already covered by a working closing order
409 day_closed the trading day is settled or being replayed — it cannot be traded
404 no_liquidation GET/DELETE with nothing running and no retained summary
400 method missing or not one of smart, aggressive

A rejection the exchange returns mid-flight does not end a live workflow: it lands in errors[] and the affected order is parked as stalled, so the rest of the book keeps closing.

For today's trading day, see the live trading API — the same paths, with idempotency keys scoped per day.