Order lifecycle
Types, time-in-force, self-trade prevention, and the status machine — every value generated from the order model so the lists cannot rot.
An order is a BUY or SELL of shares in a specific market outcome. Every table on this
page is generated directly from the order model (app/models/order.py) and drift-gated in
CI, so it always matches the enums the engine enforces.
Order types
Those are the only two order types. Drazill does not support stop, stop-limit, or trailing orders, and scalar (numeric-range) markets are defined but not yet live. If you need conditional exits, implement them in your own client against the price feed.
A MARKET order may carry an optional max_slippage_bps cap — the matcher will not fill any
share worse than reference × (1 ± bps/10000). Left null, a market order is unbounded
(today’s default).
Time-in-force
GTC is the default. IOC and FOK are taker controls; DAY expires at end of day.
Post-only is a separate maker-only flag (LIMIT orders only): a post-only order that
would immediately cross — taking liquidity from the book or the AMM — is rejected at
placement rather than executing as a taker.
Good-till-event (GTE)
GTE is the time-in-force built for event markets: work this order until the event
starts. You do not send an expiry with it — the server derives one at placement as
min(market.closes_at, kickoff), taking the kickoff when the market has one, and overrides
any expires_at you send.
The reason it is a first-class time-in-force rather than a client-side “expire at the close” convenience is drift. If operations move a market’s close time or a game is postponed, the expiry sweep re-derives the boundary before acting: a boundary that moved later advances the order’s expiry and it keeps resting; a boundary that moved earlier expires the order on the new one. A static expiry you computed at placement cannot follow either move.
GTE is declared in the enum above but not enabled on this platform yet. While it is
off, a GTE order is rejected at validation. Every other time-in-force is unaffected.
Self-trade prevention (STP)
When your own resting order would match your own incoming order, the STP mode decides what
happens. The default is CANCEL_NEWEST.
NONE (allow self-trades) is locked down for retail and near market close — a
manipulation surface. With the retail lockdown enabled, a retail NONE request is coerced
to CANCEL_NEWEST, and NONE is blocked within a configured window before close. Source:
docs/decisions/0001-unified-price-state.md (STP_NONE_RETAIL_LOCKDOWN_ENABLED).
Status machine
An order moves through these statuses:
The typical path is PENDING → OPEN → PARTIALLY_FILLED → FILLED. An order can instead end at
CANCELLED, EXPIRED, or REJECTED. There is no HALTED order status — trading halts
are a market-level state, covered in Markets & settlement.
Bounds
A limit price is bounded to 0.01–0.99 (an outcome never trades at exactly 0 or 1), and
order notional is bounded in CAD. All money fields are decimal strings ("12.50"), never
floats.
The price tick grid
A limit price must be a whole number of cents. "0.62" is valid; "0.6237" is rejected
with 422, on placement and on amendment alike. There is no configuration that relaxes this.
The reason is queue fairness, not tidiness. Price-time priority is decided at the price a
trader can see, and every surface in the product — the ladder, the ticket, the receipt —
renders prices in cents. A resting order at 0.6237 would sit ahead of the whole 0.62
queue while displaying as 0.62, so the traders it passed could not see why. The rule is
enforced twice: at the API boundary and as a database constraint, so no path can write one.
Quantity is capped at 100,000 shares per order — the same number the matching engine enforces, so a size the API accepts is a size the engine will run.
Amending a resting order
PATCH /api/v1/orders/{order_id} edits an open LIMIT order’s price, size or expiry.
quantity in the body is the new remaining (open) size, not the original total. Which
of two things happens depends entirely on what you changed — the split is the industry
convention and it is what keeps the queue honest:
A replacement is a genuinely new order: it re-runs every placement check — compliance,
halt state, exposure, self-trade prevention, funding — exactly as a fresh POST /orders
would. Nothing gets in through an amend that could not get in through a placement.
Two consequences worth planning for:
- The response may carry a different
idthan the order you amended. Treat the returned order as authoritative rather than assuming continuity. client_order_idfollows the order. A reprice or increase carries yourclient_order_idonto the replacement, so your retry-by-client-id convention keeps working across an amend. (The cancelled original gives the id up — one live order owns it at a time.)
If the order fills or is cancelled between your read and the amend, you get
409 ORDER_AMEND_CONFLICT: the fill won, and you should refetch rather than retry blindly.
The whole amend runs in one transaction, so a failed replacement rolls back the cancel too —
you can never end up with neither order.
The order_updated event
Both amend paths publish an order_updated event on your private WebSocket channel, so a
connected client learns about an edit without polling. The payload is the standard order
shape plus:
amend_kind—"in_place"(same id, queue preserved) or"replaced"(new id);replaces_order_id— on a"replaced"event, the id that is now gone.
A reprice also emits the standard order_cancelled for the old id, so a client tracking open
orders by id removes it rather than holding a phantom.
Batch placement and cancellation
POST /api/v1/orders/batch places several orders in one request, and
POST /api/v1/orders/cancel-batch cancels several by id.
A batch is N independent orders, never one compound order. Every leg runs the identical
validation and compliance stack a single POST /orders would, and one leg’s rejection never
rolls back another leg’s success. Partial results are the contract, not an edge case:
results is always the same length as the request’s orders and in the same order, so you
can zip the two without matching on ids. Each error.code is the same code the single-order
endpoint returns for that rejection, so your existing error handling works leg by leg.
- Leg cap. A batch carries at most 20 orders (the same cap the trading ticket
enforces) and a cancel batch at most 50 ids. Over the cap is a
422and nothing is placed. - Idempotency. Both endpoints are idempotent as a unit and per leg — see Idempotency.
- Rate limiting. A batch costs what its legs cost. See Rate limits.
- Cancel results.
cancel-batchreturnscancelled(the ids that were cancelled) andfailed(each with anorder_id, acodeand areason). An id you do not own fails as that id, not as the whole request. Duplicate ids collapse to one attempt.
Cancel-on-disconnect
A dead-man switch for bots: if your API key stops sending heartbeats, the orders that key placed are cancelled.
- API-key authentication is required. A session token has no key to arm, so it gets a
typed
400rather than a success response for a switch that armed nothing. - Any authenticated request from that key refreshes the heartbeat, as does a WebSocket pong on a connection that key authenticated. A working bot stays alive by doing its job; an idle-but-healthy one stays alive on its socket’s pongs.
- Scoped to the key, never to the account. Orders you placed in the web app are not touched when a bot dies.
- It fires once. After cancelling, the switch disarms; a reconnecting bot re-arms.
- It never fires on infrastructure doubt. If the heartbeat store cannot be read, nothing is cancelled — a blip must never read as “every bot died at once”. The trade-off is deliberate and it is the safe direction: if the sweep is unavailable, your orders rest.
Each cancelled order emits the standard order_cancelled event on your private channel.
Cancel-on-disconnect is not enabled on this platform yet — while it is off, all three
endpoints return 404.
Idempotent placement
Two independent mechanisms prevent duplicate orders:
client_order_id— an application-owned id you attach to an order; a repeat with the same id for your account is de-duplicated.Idempotency-Keyheader — the platform-wide idempotency guard for retryable mutations. Re-issuing a timed-out order-placement request with the same key hits the server’s replay guarantee instead of placing a second order (Idempotency-Replayedmarks a replay).
The SDK quickstarts show the Idempotency-Key pattern end to end — see
Quickstart step 4.

