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

Order typeMeaning
LIMITLimit order with specific price
MARKETMarket order at best available price

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.

Time-in-forceMeaning
GTCGood till cancelled (default)
DAYCancel at end of day
IOCImmediate or cancel (fill what you can, cancel rest)
FOKFill or kill (fill completely or cancel)
GTEGood till event (market close / kickoff boundary)

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.

STP modeMeaning
CANCEL_NEWESTCancel the incoming (taker) order
CANCEL_OLDESTCancel the resting (maker) order
CANCEL_BOTHCancel both orders
NONEAllow self-trades

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:

StatusMeaning
PENDINGOrder created, awaiting processing
OPENOrder in order book
PARTIALLY_FILLEDSome shares matched
FILLEDFully matched
CANCELLEDCancelled by user
EXPIREDOrder expired
REJECTEDOrder rejected by system

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

BoundRangeWhat it is
Limit price0.01 – 0.99Price per share, in CAD (= implied probability)
Order notional1.00 – 10,000.00Total order value, in CAD

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:

ChangeWhat happensQueue position
Decrease quantityEdited in place. The freed reservation is released to your available balance.Preserved — same order id, same created_at.
Change expires_at onlyEdited in place.Preserved.
Change priceAtomic cancel + replace: the old order is cancelled and a new one is placed.Lost — new id, back of the queue at the new price.
Increase quantityAtomic cancel + replace.Lost — new id, back of the queue.

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 id than the order you amended. Treat the returned order as authoritative rather than assuming continuity.
  • client_order_id follows the order. A reprice or increase carries your client_order_id onto 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:

POST /orders/batch response
{
"placed": 2,
"failed": 1,
"results": [
{ "index": 0, "status": "placed", "order": { "id": "…", "status": "OPEN" }, "error": null },
{ "index": 1, "status": "failed", "order": null,
"error": { "code": "INSUFFICIENT_BALANCE", "message": "…" } },
{ "index": 2, "status": "placed", "order": { "id": "…", "status": "OPEN" }, "error": null }
]
}

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 422 and 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-batch returns cancelled (the ids that were cancelled) and failed (each with an order_id, a code and a reason). 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.

EndpointEffect
POST /api/v1/orders/cancel-on-disconnectArm the switch with a ttl_seconds.
GET /api/v1/orders/cancel-on-disconnectReport whether it is armed, and how long you may stay silent.
DELETE /api/v1/orders/cancel-on-disconnectDisarm it.
  • API-key authentication is required. A session token has no key to arm, so it gets a typed 400 rather 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-Key header — 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-Replayed marks a replay).

The SDK quickstarts show the Idempotency-Key pattern end to end — see Quickstart step 4.