Equity Trading
Place, monitor, and cancel equity orders against TradeZero's order-management system using the endpoints in the table below. The API supports all four trader actions (Buy, Sell, Short, Cover), four order types, eight time-in-force values, and covers everything from a single limit order to advanced cancel patterns and full portfolio liquidation.
At a glance
| Method | Path | Purpose |
|---|---|---|
GET | /v1/api/accounts/{accountId}/routes | List available routing destinations and the order-type / TIF combinations each supports |
GET | /v1/api/accounts/{accountId}/orders | Today's orders (all statuses) |
GET | /v1/api/accounts/{accountId}/order/{clientOrderId} | Retrieve a single order by clientOrderId |
GET | /v1/api/accounts/{accountId}/orders/start-date/{startDate} | Historical fills from a specific date onward (one row per execution; up to 1 week) |
GET | /v1/api/accounts/{accountId}/orders-with-pagination/start-date/{startDate} | Paginated historical order history (up to 1 year; see below) |
POST | /v1/api/accounts/{accountId}/order | Place a new order |
DELETE | /v1/api/accounts/{accountId}/orders/{orderId} | Cancel a specific order by clientOrderId |
DELETE | /v1/api/accounts/orders | Cancel all open orders (or all for a specific symbol) |
GET | /v1/api/accounts/{accountId}/is-easy-to-borrow/symbol/{symbol} | Pre-trade borrow check before placing a short |
Authentication uses two request headers on every call - see Authentication for setup. Open positions and P&L are covered on the Open Positions page.
Live orders can reject asynchronously - POST /order may return PendingNew before the reason appears. Read Order rejections for where text, startTime, and lastUpdated surface across REST and the Portfolio WebSocket.
Trader actions
Four trader actions drive equity orders. Each maps to a combination of side and openClose on the request body:
| Action | side | openClose | When to use |
|---|---|---|---|
| Buy | Buy | Open | Enter a new long position |
| Sell | Sell | Close | Exit an existing long position |
| Short | Sell | Open | Enter a new short position (borrows shares) |
| Cover | Buy | Close | Exit an existing short position |
Pre-trade borrow check
GET /v1/api/accounts/{accountId}/is-easy-to-borrow/symbol/{symbol}
Before placing a Short order (side: "Sell", openClose: "Open"), call this endpoint to determine whether shares are available to borrow.
curl 'https://webapi.trade0.click/v1/api/accounts/TZP12345678/is-easy-to-borrow/symbol/TSLA' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}'
{ "isEasyToBorrow": true }
| Field | Type | Meaning |
|---|---|---|
isEasyToBorrow | boolean | true - shares are freely available; you can place a short order without a locate. false - the symbol is hard-to-borrow; you must reserve shares through the Short Locates workflow before the short order will be accepted. |
If isEasyToBorrow is false, the short order returns orderStatus: "Rejected" without a prior locate reservation. See Short Locates for the full reserve workflow.
HTTP semantics
| Condition | Status | Body |
|---|---|---|
Successful POST /order (order received, even if orderStatus is Rejected) | 200 | JSON |
Successful GET | 200 | JSON |
Successful DELETE /accounts/orders (cancel-all) | 200 | {"message":"Cancel Request Submitted Successfully"} |
| JSON Schema validation error (bad enum, wrong type, out-of-range quantity) | 400 | Plain text, bullet format |
Account ID mismatch in POST /order path | 400 | {"statusCode":"BadRequest","message":"PlaceOrderWithResponse","detail":"Account for User was not found, or User doesn't have entitlements."} |
Account ID mismatch in DELETE /orders/{id} path | 400 | {"statusCode":"BadRequest","message":"CancelOrderWithResponse","detail":"Unable to fetch account orders from server. Account for User was not found, or User doesn't have entitlements."} |
Missing auth headers on DELETE /orders/{id} | 401 | {"statusCode":"Unauthorized","message":"Token not provided","detail":null} |
Missing or invalid auth headers on GET and POST endpoints | 404 | Not found (plain text) |
Account ID mismatch on GET /orders | 404 | Not found (plain text) |
Wrong HTTP method on /order (GET, PUT, PATCH) | 405 | 405 method not allowed (plain text) |
OPTIONS on /order (CORS preflight) | 200 | empty body, Allow: POST header |
Wrong HTTP method on /orders (e.g. POST against the plural endpoint) | 404 | Not found (plain text) |
HEAD on /orders | 405 | empty body |
URL path with uppercase segments (e.g. /V1/API/...) | 404 | Not found - the path is case-sensitive |
All successful responses carry Content-Type: application/json; charset=utf-8. Validation errors (400) carry Content-Type: text/plain.
Three behaviors to account for in your client:
- Some
400bodies include an internal handler label inmessage(for examplePlaceOrderWithResponseorCancelOrderWithResponse). Treatdetailand the HTTP status as the integrator-facing signal; the handler name is diagnostic only. POST /orderreturns200 OKwhen the request body is valid. ReadorderStatusin the response to see whether the order was accepted for routing. Rejected orders also return HTTP200withorderStatus: "Rejected". On live routed orders, POST may returnPendingNewfirst - pollGET /order/{clientOrderId}for the final reason. See Order rejections.- Auth failures return different status codes by endpoint.
GETandPOSTendpoints return404 Not foundwhen auth headers are missing or invalid. TheDELETE /orders/{id}cancel endpoint returns401 Unauthorizedwith a JSON body instead. Handle both. - Account mismatch on
POST/DELETEreturns400, not404. AGETwith the wrong account ID returns404; aPOST /orderorDELETE /orders/{id}with an account that doesn't match your keys returns a400JSON body. The two endpoints share the same{statusCode, message, detail}envelope but use differentmessagevalues (PlaceOrderWithResponseforPOST /order,CancelOrderWithResponseforDELETE /orders/{id}). For TradeZero America accounts, this same400appears when the portal login is used instead of the2TZaccount number - use theaccountvalue fromGET /v1/api/accounts(see Account Information).
Place an order
POST /v1/api/accounts/{accountId}/order
Places a new equity order. The endpoint is stateful - the response captures the initial order state immediately after submission. HTTP 200 means the request was accepted; read orderStatus to see whether the order was routed and filled.
Request
curl 'https://webapi.trade0.click/v1/api/accounts/TZP12345678/order' \
-X POST \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}' \
-d '{
"securityType": "Stock",
"symbol": "AAPL",
"side": "Buy",
"openClose": "Open",
"orderType": "Market",
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "my-buy-001"
}'
curl 'https://webapi.trade0.click/v1/api/accounts/TTE12345678/order' \
-X POST \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}' \
-d '{
"securityType": "Stock",
"symbol": "SPY",
"side": "Sell",
"openClose": "Open",
"orderType": "Limit",
"orderQuantity": 10,
"timeInForce": "GoodTillCancel",
"limitPrice": 500.00,
"clientOrderId": "my-short-002",
"route": "SMART"
}'
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
securityType | string | Yes | "Stock" for equities and ETFs. Use "Option" or "Mleg" for options (see Options Trading). Case-sensitive. |
symbol | string | Yes | Ticker symbol. Regex ^[a-zA-Z0-9._-]+$ - alphanumerics, dot, dash, and underscore. Leading/trailing whitespace fails the pattern; tabs or newlines inside the JSON string fail JSON parsing. Symbols with / (e.g. BRK/B) are rejected - use the dot form (BRK.B). The server preserves case verbatim and does not normalize - always send symbols in the exchange's canonical uppercase form (e.g. "AAPL", not "aapl"). |
side | string | Yes | "Buy" or "Sell". Case-sensitive. See Trader actions for how Buy/Sell/Short/Cover map to this field. |
openClose | string | Yes† | "Open" to enter a new position; "Close" to exit one. See Trader actions. Send "Open" or "Close" with that exact casing in production code. |
orderType | string | Yes | "Market", "Limit", "Stop", or "StopLimit". Case-sensitive. |
orderQuantity | number | Yes | Integer share count. Minimum 1, maximum 1,000,000. Fractional shares are not supported - sending 0.5 or any non-integer value returns 400 Bad Request with - orderQuantity: Invalid type. Expected: integer, given: number. |
timeInForce | string | Yes | One of the eight values in Time in force. Case-sensitive. |
limitPrice | number | Conditional | Required when orderType is "Limit" or "StopLimit". Omitting it on a Limit order places the order with limitPrice: 0, which the venue rejects. The JSON validator accepts up to four decimal places (e.g. 250.0001); tick-size rules are enforced at the venue. Send two decimals for stocks priced at or above $1.00 and up to four decimals for sub-$1.00 stocks. |
stopPrice | number | Conditional | Required when orderType is "Stop" or "StopLimit". |
clientOrderId | string | Recommended | Your identifier for the order. If omitted, the server generates a numeric one. Used for cancel-by-id and deduplication. See Order identity. |
route | string | Recommended | Routing destination, e.g. "SMART" for smart equity routing on a live account. Send an explicit route taken from Get available routes. On live accounts, always include route - omitting it can leave the order with no route assigned and produce R54: Unable to reach the destination Route during routing. Route names vary by account - some accounts have no SMART route at all - so query /routes and send a routeName it returns rather than hard-coding one. Paper accounts auto-assign PAPER/PAPERM when omitted. |
Response
The response body is a JSON object representing the initial state of the order. Check orderStatus to determine what happened.
{
"accountId": "TZP12345678",
"canceledQuantity": 0,
"clientOrderId": "my-buy-001",
"executed": 0,
"lastPrice": 0,
"lastQuantity": 0,
"lastUpdated": "2026-05-13T14:40:17.373Z",
"leavesQuantity": 1,
"legCount": 0,
"legs": null,
"limitPrice": 0,
"maintenanceRequirement": 0,
"marginRequirement": 0,
"maxDisplayQuantity": 0,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "PendingNew",
"orderType": "Market",
"pegDifference": 0,
"pegOffsetType": "Price",
"priceAvg": 0,
"priceStop": 0,
"route": "PAPER",
"securityType": "Stock",
"side": "Buy",
"startTime": "2026-05-13T14:40:17.373Z",
"strikePrice": 0,
"symbol": "AAPL",
"text": null,
"timeInForce": "Day",
"tradedSymbol": "AAPL"
}
The live limit-short example above echoes back the live account id and the route you sent:
{
"accountId": "TTE12345678",
"canceledQuantity": 0,
"clientOrderId": "my-short-002",
"executed": 0,
"lastPrice": 0,
"lastQuantity": 0,
"lastUpdated": "2026-05-13T14:40:17.373Z",
"leavesQuantity": 10,
"legCount": 0,
"legs": null,
"limitPrice": 500.00,
"maintenanceRequirement": 0,
"marginRequirement": 0,
"maxDisplayQuantity": 0,
"openClose": "Open",
"orderQuantity": 10,
"orderStatus": "PendingNew",
"orderType": "Limit",
"pegDifference": 0,
"pegOffsetType": "Price",
"priceAvg": 0,
"priceStop": 0,
"route": "SMART",
"securityType": "Stock",
"side": "Sell",
"startTime": "2026-05-13T14:40:17.373Z",
"strikePrice": 0,
"symbol": "SPY",
"text": null,
"timeInForce": "GoodTillCancel",
"tradedSymbol": "SPY"
}
The response shape is the same on paper and live - only accountId and route differ.
{
"orderStatus": "Rejected",
"text": "R114: Invalid duplicate UserOrderId",
"clientOrderId": "my-buy-001",
"canceledQuantity": 1,
"executed": 0,
"leavesQuantity": 0
}
Response fields
| Field | Type | Notes |
|---|---|---|
accountId | string | The account the order was placed on. |
clientOrderId | string | Your identifier, or a server-generated numeric string if you omitted it. Always assign your identifier with clientOrderId - orderId in the request body is not a recognized assignment field and the server will return its own auto-generated clientOrderId if you set it. |
orderStatus | string | "PendingNew" - accepted, routing in progress; "New" - acknowledged at venue, resting in book; "Filled" - fully executed; "Canceled" - successfully canceled; "Rejected" - declined (read text for reason). |
text | string | null | Human-readable reason when orderStatus is "Rejected". null on accepted or in-flight orders. May use an R##: prefix or a plain-text platform message (see Order rejections). |
executed | number | Shares filled so far. |
leavesQuantity | number | Shares still open. 0 once the order is terminal. |
canceledQuantity | number | Shares canceled. |
orderQuantity | number | Original quantity requested. |
limitPrice | number | The limit price sent. 0 if not a limit order. |
priceStop | number | The stop price sent. 0 if not a stop order. |
priceAvg | number | Volume-weighted average fill price. 0 while unfilled. |
openClose | string | "Open", "Close", or "Unknown" (the server may normalize to "Unknown" in some early-rejection scenarios). |
route | string | The routing destination the server used. Echoes the route name ("PAPER", "SMART", …) on accepted orders and on rejected orders that reached a venue. Validation-time rejections show the literal "<no value>" when no venue was selected - treat rejection by checking orderStatus === "Rejected" and reading text. |
startTime | string | ISO 8601 timestamp when the order was received. |
lastUpdated | string | ISO 8601 timestamp of the most recent state change. |
tradedSymbol | string | For stocks, equals symbol. For options, the OCC contract string. |
strikePrice | number | 0 for equities. |
legCount | number | 0 for single-leg orders. |
legs | array | null | null for single-leg orders. |
Get today's orders
GET /v1/api/accounts/{accountId}/orders
Returns every working and session-visible order for the account - New, Accepted, PartiallyFilled, and other non-terminal states, plus any still-working orders carried over from earlier sessions (multi-day GoodTillCancel / GTC_Plus orders that have not filled or expired). Filled market orders may drop out of this list quickly; for a reliable status check on a specific order, use GET /order/{clientOrderId} instead. The response is wrapped in an orders key.
curl 'https://webapi.trade0.click/v1/api/accounts/TZP12345678/orders' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}'
{
"orders": [
{
"accountId": "TZP12345678",
"canceledQuantity": 0,
"clientOrderId": "my-buy-001",
"executed": 0,
"lastPrice": 0,
"lastQuantity": 0,
"lastUpdated": "2026-05-14T15:36:36.987036Z",
"leavesQuantity": 1,
"legCount": 0,
"legs": null,
"limitPrice": 1,
"maintenanceRequirement": 0,
"marginRequirement": 0,
"maxDisplayQuantity": 0,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "New",
"orderType": "Limit",
"pegDifference": 0,
"pegOffsetType": "Price",
"priceAvg": 0,
"priceStop": 0,
"route": "PAPER",
"securityType": "Stock",
"side": "Buy",
"startTime": "2026-05-14T15:36:36.8753891Z",
"strikePrice": 0,
"symbol": "F",
"text": "TRAFIX_SIM",
"timeInForce": "Day",
"tradedSymbol": "F"
}
]
}
The same field names appear on live accounts (accountId set to your live id, route set to the route you sent).
Order shape across REST and WebSocket
GET /orders rows use the same field names as POST /order and GET /order/{clientOrderId} - including a top-level clientOrderId and accountId. Read clientOrderId directly; no parsing required.
Portfolio WebSocket Order pushes use a WebSocket field set. Normalize these when merging stream updates with REST data:
Clean REST field (POST /order, GET /order/{cid}, GET /orders) | Portfolio WebSocket Order push | Notes |
|---|---|---|
accountId | account | Same value, different key. |
clientOrderId | userOrderId, format "{accountId}:{clientOrderId}" | Split userOrderId on the first : to recover the bare id. |
canceledQuantity | cancelledQuantity | Spelling difference. |
lastQuantity | lastQty | |
leavesQuantity | leavesQuantity and lvsQty | Both present on WS rows. |
maxDisplayQuantity | maxDisplayQty | |
orderStatus | status | Same value; prefer orderStatus in new code. |
WebSocket rows also carry extra fields (accountType, legIndex, mlegID, startTimeET, …) not present on the clean REST shape.
GET /orders/start-date/{date} is different again - each row is a fill-level trade record (tradeId, qty, price, fees), not an order object. See Get historical orders.
| Field note | Detail |
|---|---|
tradedSymbol | Equals symbol for stocks. For options, symbol is the underlying ticker (e.g. "AAPL") and tradedSymbol carries the full OCC contract (e.g. "AAPL260717C00600000"). |
text | Plain message string, or null. Carries the rejection reason for Rejected rows (R##: … or plain rejection text) and is null on accepted rows. On paper, accepted rows may carry a simulator placeholder in text instead of null. See Order rejections. |
legCount / legs | 0 and null for single-leg equity and option orders. Multi-leg orders populate these once the spread is accepted. |
Get a single order
GET /v1/api/accounts/{accountId}/order/{clientOrderId}
Retrieve a specific order by its clientOrderId. This is more efficient than fetching all of today's orders and filtering client-side when you already know the id you're looking for.
curl 'https://webapi.trade0.click/v1/api/accounts/TZP12345678/order/my-buy-001' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}'
The response is the order object directly (not wrapped in an orders array). For stock and single-leg option orders - and for Mleg orders before their legs[] array is populated - the shape matches the POST /order response exactly. For filled multi-leg orders with populated leg data, the response may switch to the WebSocket field shape (account, userOrderId, cancelledQuantity, …) - see Options → Response. Returns 404 Not found if the clientOrderId is not found on the account.
Get historical orders
GET /v1/api/accounts/{accountId}/orders/start-date/{startDate}
Retrieves up to one week of historical orders for your account. Only orders for live production accounts are returned - paper trading accounts do not have order history available and always respond with { "orders": [] }.
Each row in the response represents an individual fill, not the originating order - the field set is different from GET /orders. Every row carries tradeId, qty, price, commission, totalFees, grossProceeds, netProceeds, tradeDate. There is no clientOrderId, no userOrderId, and no orderStatus on these rows, and a partial-filled order produces multiple rows (one per execution). Use this endpoint for post-trade reconciliation, daily P&L, fee/commission rollups, and audit trails. The live working-order list - including multi-day GTC orders placed in earlier sessions - lives in GET /orders.
Accepts ISO 8601 date format (YYYY-MM-DD). An ISO timestamp with a time component (URL-encoded) is also accepted.
curl 'https://webapi.trade0.click/v1/api/accounts/TTE12345678/orders/start-date/2026-05-01' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}'
{
"orders": [
{
"tradeId": 238369493,
"accountId": "TTE12345678",
"symbol": "TSLA",
"securityType": "Stock",
"side": "Buy",
"qty": 1,
"price": 438.59,
"grossProceeds": -438.59,
"netProceeds": -439.62,
"commission": 0,
"totalFees": 0.99,
"currency": "USD",
"tradeDate": "2026-05-13T00:00:00",
"settleDate": "2026-05-14T00:00:00",
"entryDate": "2026-05-13T00:00:00",
"execTime": "06:10:12",
"canceled": false,
"mLegId": 0,
"spreadType": 0,
"notes": ""
}
]
}
| Field | Type | Notes |
|---|---|---|
tradeId | integer | Unique identifier for this individual fill (the primary key on every row). |
accountId | string | Account the trade settled on. |
symbol | string | Symbol traded. |
securityType | string | "Stock" or "Option". |
side | string | "Buy" or "Sell" (no enriched "SellShort" / "BuyToCover" here). |
qty | integer | Shares filled on this execution (not the originating order's quantity). |
price | number | Per-share fill price. |
grossProceeds | number | qty × price with sign - negative on Buys (cash out), positive on Sells. |
netProceeds | number | grossProceeds minus totalFees and commission, sign-preserved. |
commission | number | Commission charged on this fill (0 on commission-free accounts). |
totalFees | number | All non-commission fees (regulatory, exchange, etc.). |
currency | string | Settlement currency ("USD"). |
tradeDate | string (date-time) | When the trade executed. |
settleDate | string (date-time) | T+1 settlement date. |
entryDate | string (date-time) | When the originating order was entered. |
execTime | string | HH:MM:SS local time of the execution. |
canceled | boolean | true if the trade was busted/cancelled post-execution; skip these for activity counts. |
mLegId | integer | Multi-leg ticket ID. 0 for single-leg orders. |
spreadType | integer | Internal spread classifier; 0 for ordinary single-leg fills. |
notes | string | Free-form notes string from the execution venue; empty unless the venue attaches a note. |
The historical-orders archive is populated only by live trades. Paper accounts always return { "orders": [] } here regardless of the date. To test reconciliation logic on paper, fall back to today's working/closed rows from GET /orders (different shape - see "Get today's orders").
Date format compatibility:
| Format | Example | Accepted |
|---|---|---|
YYYY-MM-DD | 2026-05-01 | ✓ |
ISO 8601 with T and Z | 2026-05-01T00:00:00Z | ✓ |
| ISO 8601 with UTC offset (URL-encoded) | 2026-05-01T00%3A00%3A00-04%3A00 | ✓ |
| Unpadded month/day | 2026-5-1 | ✓ |
US-style MM-DD-YYYY | 05-01-2026 | ✓ |
| Far future | 2099-12-31 | ✓ (empty array) |
| Unix epoch start | 1970-01-01 | ✓ (empty array) |
Compact YYYYMMDD | 20260501 | ✗ - returns 404 |
| Non-date string | not-a-date | ✗ - returns 404 |
Use YYYY-MM-DD as the canonical format. Compact YYYYMMDD without separators is not accepted.
Paginated historical orders
For longer lookbacks and large result sets, use the paginated variant:
GET /v1/api/accounts/{accountId}/orders-with-pagination/start-date/{startDate}
| Query param | Default | Notes |
|---|---|---|
numberOfDays | 30 | Window length from startDate, up to 365 days |
offset | 0 | Rows to skip (pagination) |
limit | 100 | Page size (max 100) |
symbol | - | Optional symbol filter |
The response wraps rows in a tradingHistory array and includes a pagination object (totalRecords, currentOffset, currentLimit). Each row uses the same fill-level field set as GET /orders/start-date/{startDate} above - one row per execution, no clientOrderId. Live accounts only; paper returns empty history. Rate limit: 1 request/s per account (API Rate Limits). Full parameter and schema details: Retrieve Historical Orders Paginated.
Common patterns
End-of-day P&L recap. Pull the day's historical-order rows, skip rows where canceled is true, sum netProceeds for realised cash flow, and group by symbol + qty × price for a per-symbol VWAP:
async function dailyRecap(accountId: string, date: string) {
const res = await fetch(
`/v1/api/accounts/${accountId}/orders/start-date/${date}`,
{ headers: authHeaders() },
);
const { orders: rows } = await res.json() as { orders: HistoricalOrderRow[] };
const filled = rows.filter(r => !r.canceled);
const cashFlow = filled.reduce((a, r) => a + r.netProceeds, 0);
const fees = filled.reduce((a, r) => a + r.totalFees + r.commission, 0);
return { fills: filled.length, cashFlow, fees };
}
Full audit trail. For compliance exports or trade reconciliation, paginate by week (one week is the maximum history per request) and persist every row keyed on tradeId. Two fills on the same symbol at the same execTime from a partial-filled order will have distinct tradeIds.
To correlate a historical-orders row back to the originating order, match by symbol + tradeDate + side + qty + price against rows from GET /orders - there is no foreign key linking these rows to clientOrderId.
Cancel a specific order
DELETE /v1/api/accounts/{accountId}/orders/{orderId}
Attempts to cancel the order identified by orderId in the path. Pass the clientOrderId you assigned when placing the order.
curl 'https://webapi.trade0.click/v1/api/accounts/TZP12345678/orders/my-buy-001' \
-X DELETE \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}'
Cancel-by-id has three different failure shapes:
-
401 Unauthorized(JSON body) - auth headers are missing:{ "statusCode": "Unauthorized", "message": "Token not provided", "detail": null } -
404 Not found(plain text) - the order was not found. This covers: wrongclientOrderId, the order is not yet registered (pollGET /ordersafterPOST /orderbefore canceling), the order is already terminal (Filled,Canceled,Expired), or auth headers that do not grant access.Rejectedorders are a special case - cancel may returnHTTP 200and overwritetextwith R130 instead of404. Do not cancel rejected orders. -
400 Bad Request(JSON body) - the path account ID doesn't match the keys you authenticated with (account mismatch on cancel):{"statusCode": "BadRequest","message": "CancelOrderWithResponse","detail": "Unable to fetch account orders from server. Account for User was not found, or User doesn't have entitlements."}
In all cases, poll GET /orders after the attempt to confirm the actual orderStatus. Do not infer success or failure solely from the cancel response.
For reliable bulk cancellation, use DELETE /v1/api/accounts/orders instead.
textIf the order is already terminal (Rejected, Filled, Canceled, Expired), DELETE /orders/{clientOrderId} may return HTTP 200 and replace the original rejection reason with R130: Cancel Request Rejected: …. Read the placement rejection from GET /order/{clientOrderId} before attempting cancel, and never cancel orders in Rejected status. See Order rejections → Do not cancel already-rejected orders.
On success, the response is a JSON object showing the canceled order's state, with canceledQuantity equal to the original orderQuantity. On failure, 404 Not found.
Cancel all orders
DELETE /v1/api/accounts/orders
Cancels all open orders on the account. Optionally scope the cancel to a single symbol with a ?symbol= query parameter. This endpoint uses a multipart/form-data body - not JSON - to pass the account ID.
curl 'https://webapi.trade0.click/v1/api/accounts/orders' \
-X DELETE \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}' \
-F 'account=TZP12345678'
curl 'https://webapi.trade0.click/v1/api/accounts/orders?symbol=AAPL' \
-X DELETE \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}' \
-F 'account=TZP12345678'
{
"message": "Cancel Request Submitted Successfully"
}
const form = new FormData();
form.append('account', accountId);
const res = await fetch(
'https://webapi.trade0.click/v1/api/accounts/orders',
{
method: 'DELETE',
headers: {
Accept: 'application/json',
'TZ-API-KEY-ID': apiKey,
'TZ-API-SECRET-KEY': apiSecret,
// Don't set Content-Type yourself - FormData generates the
// correct multipart/form-data boundary automatically.
},
body: form,
}
);
The account field in the body is required. Both multipart/form-data and application/x-www-form-urlencoded are accepted; sending a JSON body, a plain-text body, or omitting the account field entirely returns 404. The cancel is asynchronous - the 200 response confirms the request was received, not that orders are already gone. Poll GET /orders after a short delay to confirm.
Get available routes
GET /v1/api/accounts/{accountId}/routes
Returns the routing destinations available on the account along with each route's supported order types, security types, and time-in-force values.
curl 'https://webapi.trade0.click/v1/api/accounts/TZP12345678/routes' \
-H 'TZ-API-KEY-ID: {YOUR_PUBLIC_KEY}' \
-H 'TZ-API-SECRET-KEY: {YOUR_SECRET}'
The route inventory you get back depends on whether the credentials are paper or live - paper accounts expose a small synthetic set, live accounts expose real venues:
{
"routes": [
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit"],
"routeName": "PAPER",
"securityTypes": ["Stock", "Option"],
"timesInForce": ["Day", "GoodTillCancel", "GoodTillCrossing"],
"useDisplayQty": false
},
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit"],
"routeName": "PAPERM",
"securityTypes": ["MLEG"],
"timesInForce": ["Day"],
"useDisplayQty": false
}
]
}
{
"routes": [
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "MarketOnClose", "LimitOnClose"],
"routeName": "SMART",
"securityTypes": ["Stock"],
"timesInForce": ["Day", "GoodTillCancel", "AtTheOpening", "Day_Plus", "GTC_Plus"],
"useDisplayQty": true
},
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit"],
"routeName": "CTDL",
"securityTypes": ["Stock"],
"timesInForce": ["Day", "GoodTillCancel", "GoodTillCrossing"],
"useDisplayQty": false
},
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit"],
"routeName": "SMARTO",
"securityTypes": ["Option"],
"timesInForce": ["Day", "GoodTillCancel"],
"useDisplayQty": false
},
{
"orderTypes": ["Market", "Limit"],
"routeName": "SMARTM",
"securityTypes": ["Option", "MLEG"],
"timesInForce": ["Day"],
"useDisplayQty": false
},
{
"orderTypes": ["Market", "Limit", "LimitOnClose"],
"routeName": "ARCA",
"securityTypes": ["Stock"],
"timesInForce": ["Day", "GoodTillCancel", "ImmediateOrCancel", "FillOrKill", "GoodTillCrossing"],
"useDisplayQty": true
}
]
}
A live account exposes routes such as:
| Route | Security types | Notes |
|---|---|---|
SMART | Stock | TradeZero's smart order router for equities. Supports the widest set of equity TIFs (Day_Plus, GTC_Plus, AtTheOpening, MarketOnClose, LimitOnClose) and advertises useDisplayQty: true for iceberg orders. |
CTDL | Stock | Direct-access route to a single venue. Useful when you want deterministic routing rather than smart-router discretion. |
SMARTO | Option | Smart router for single-leg options. |
SMARTM | Option, MLEG | Smart router for multi-leg option spreads. Day-only. |
ARCA | Stock | Direct ECN route. Adds ImmediateOrCancel and FillOrKill TIFs that aren't on SMART. useDisplayQty: true. |
The exact route set on a given live account depends on the account's subscription and configuration - query /routes and use what you actually get back rather than hard-coding a list. Route names differ from account to account, and some accounts have no SMART route at all, so always send a routeName that /routes returned for your account - for example "route": "SMART" for smart equity routing where it's available.
Send an explicit route on live accounts. Live orders submitted without route can be accepted with no route assigned and then reject with R54: Unable to reach the destination Route during routing. Paper accounts auto-assign PAPER for stocks/single-leg options and PAPERM for multi-leg option spreads when route is omitted. Sending an explicit route everywhere keeps your code portable between paper and live.
POST /order accepts only "Market", "Limit", "Stop", and "StopLimit". GET /routes may still list other orderTypes (including "TrailStop" and "RangeOrder"); those are not accepted on place-order and must not be sent. Use /routes to discover routing destinations and their supported time-in-force values; send one of the four order types above when placing an order.
Order types
orderType | Required price fields | Notes |
|---|---|---|
Market | None | Executes at the best available price. Rejected outside regular trading hours (9:30 AM-4:00 PM ET) on both paper and live accounts. |
Limit | limitPrice | Executes at limitPrice or better. Rests in the book if the price is not immediately available. |
Stop | stopPrice | Becomes a market order when stopPrice is touched. |
StopLimit | stopPrice + limitPrice | Becomes a limit order at limitPrice when stopPrice is touched. |
Market orders outside regular hours: Submitting a Market order before 9:30 AM ET or after 4:00 PM ET returns HTTP 200, but the body will contain "orderStatus": "Rejected" and "text": "R78: Market orders are not allowed at this time". Nothing fills, nothing rests. This rule applies identically to paper and live accounts.
Time in force
Eight timeInForce values are accepted. All values are case-sensitive.
timeInForce | Common name | When the order is active |
|---|---|---|
Day | Day | Regular session only (9:30 AM-4:00 PM ET). Expires at close if unfilled. |
GoodTillCancel | GTC | Persists across sessions until manually canceled or filled. Valid for Limit, Stop, and StopLimit on SMART routing. |
Day_Plus | Day+ | Extended-hours session (4:00 AM-8:00 PM ET). Limit orders only. For pre/post-market trading, submit Limit orders with Day_Plus (convert Market → Limit, Day → Day_Plus). |
GTC_Plus | GTC+ | Like GTC but active during extended hours. Limit orders only. |
AtTheOpening | OPG / MOO / LOO | Submits during 4:00 AM-9:25 AM ET; executes at the symbol's opening print. With Market behaves as Market-on-Open (MOO); with Limit as Limit-on-Open (LOO). Not available on paper accounts. |
ImmediateOrCancel | IOC | Fill immediately or cancel the unfilled portion. Limit orders during regular hours. Not available on paper accounts. |
FillOrKill | FOK | Fill the entire quantity immediately or cancel the whole order. Direct routing only; not available on SMART or paper accounts. |
GoodTillCrossing | GTX | Deprecated on SMART routing. If used on a non-SMART direct route, means "valid for the current trading session." Avoid on SMART. |
AtTheOpening (MOO/LOO), ImmediateOrCancel, and FillOrKill are not available on paper accounts. Placing them on a paper account returns HTTP 200 with "orderStatus": "Rejected". On live accounts, these TIFs work as described above.
Order rejections
When the order engine declines an order, the outcome is always HTTP 200 with an order object in the body - never a dedicated rejection HTTP status. The orderStatus field carries the outcome; the human-readable reason lives in text. Timestamps are on every row: startTime (submission) and lastUpdated (last state change, including when text is populated).
POST /order response aloneOn live routed orders (when you send an explicit route such as "SMART"), rejections fall into two patterns:
-
Asynchronous (common for TradeZero route checks such as R24, R54, R95, and platform NBBO limits):
POST /orderreturnsPendingNew+text: null, thenGET /order/{clientOrderId}showsRejectedwithtextwithin ~50 ms. -
Synchronous reject without reason text (common for R118 with an explicit route on live):
POST /orderreturnsorderStatus: "Rejected"andtext: null, and the server rewritesclientOrderIdby appending anINVALIDsuffix and a timestamp (for examplemy-order-idINVALID01/15/2026 09:30:00).GET /order/{yourOriginalClientOrderId}returns404even after polling. The row may appear only onGET /orders— still withtext: null. The sameINVALIDsuffix rewrite can occur on live R114 duplicate-clientOrderIdPOSTs. Clients who only pollGET /orderwith the original id may miss the rejection until a cancel attempt surfaces R130.
R118 without an explicit route follows the async pattern instead: PendingNew → Rejected with full "R118: Stop price is on wrong side of quote" on GET.
Always read clientOrderId from the POST response for subsequent lookups — it may differ from the ID you sent. If single-order GET returns 404, scan GET /orders for a Rejected row with matching symbol, side, and startTime.
Where rejection reasons appear
| Surface | When to use | Rejection behavior |
|---|---|---|
POST /order response | Immediate ack | May show PendingNew + text: null even when the order will reject. Sync rejections (R114, R145, …) include text here. R118 with route may return sync Rejected + text: null and a rewritten clientOrderId. |
GET /order/{clientOrderId} | Single-order lookup | Authoritative when it returns 200. Poll after POST until terminal. Returns 404 if you use the pre-POST ID after an INVALID suffix rewrite — use the POST response clientOrderId or fall back to GET /orders. May return 404 for a few milliseconds right after POST on normal async rejections — retry before treating as missing. |
GET /orders | Today's full order book | Same rejection rows as single-order lookup when GET works. May be the only surface for R118 rejections sent with an explicit route (routed stop on wrong side). Can show Rejected before GET /order catches up on some async routed rejections. |
Portfolio WebSocket Order push | Real-time blotter | Pushes two updates for many async rejections: first PendingNew, then Rejected with text. Subscribe before placing orders. See Portfolio Stream → Order rejections. |
DELETE /orders/{clientOrderId} | Cancel attempt | Not a placement rejection channel. Canceling an already-terminal order returns HTTP 200 but may overwrite text with R130 (see below). |
Sync vs async rejection lifecycle
Synchronous (reason on POST):
POST /order → orderStatus: "Rejected", text: "R145: Negative Price Is Not Allowed"
GET /order → same row, same text
Asynchronous (common on live routed orders — R24, R54, R95, platform NBBO; typical with route: "SMART"):
POST /order → orderStatus: "PendingNew", text: null
↓ ~50 ms
GET /order/{cid} → orderStatus: "Rejected", text: "R24: Your order cannot have a STOP price of Zero"
GET /orders → same row in orders[]
Portfolio WS Order → PendingNew push (no text), then Rejected push with text (if subscribed)
R118 without route follows the async pattern with full text:
POST /order → orderStatus: "PendingNew", text: null
↓ ~6 ms
GET /order/{cid} → orderStatus: "Rejected", text: "R118: Stop price is on wrong side of quote"
Synchronous reject without reason text (R118 with explicit route on live):
POST /order → orderStatus: "Rejected", text: null,
clientOrderId: "my-idINVALID07/02/2026 17:12:28" ← rewritten from "my-id"
GET /order/{originalId} → 404 (not found)
GET /order/{post.clientOrderId} → 404 (not found)
GET /orders → Rejected row present, text: null, rewritten clientOrderId
Integrators who poll only GET /order/{sentClientOrderId} will see 404 and no rejection reason. Do not cancel — that surfaces R130 instead.
Portfolio WebSocket: two pushes for async rejections
On live routed orders, the Portfolio stream delivers two Order updates for the same clientOrderId:
{
"action": "update",
"subscription": "Order",
"order": {
"clientOrderId": "my-stop-001",
"orderStatus": "PendingNew",
"text": null,
"symbol": "BABA",
"startTime": "2026-01-15T14:31:10.419+00:00",
"lastUpdated": "2026-01-15T14:31:10.419+00:00"
}
}
{
"action": "update",
"subscription": "Order",
"order": {
"clientOrderId": "my-stop-001",
"orderStatus": "Rejected",
"text": "R118: Stop price is on wrong side of quote",
"symbol": "BABA",
"startTime": "2026-01-15T14:31:10.419+00:00",
"lastUpdated": "2026-01-15T14:31:10.423+00:00"
}
}
Use lastUpdated on the Rejected push as the rejection timestamp. Ignore the PendingNew push for user-facing error display - wait for terminal status or poll REST. Full WebSocket details: Portfolio Stream.
async function awaitOrderTerminal(accountId: string, clientOrderId: string) {
const deadline = Date.now() + 5_000;
while (Date.now() < deadline) {
const res = await fetch(
`${BASE}/v1/api/accounts/${accountId}/order/${encodeURIComponent(clientOrderId)}`,
{ headers: AUTH_HEADERS },
);
if (res.status === 404) {
await sleep(50);
continue; // registration race - retry
}
const order = await res.json();
if (order.orderStatus !== 'PendingNew') return order;
await sleep(50);
}
throw new Error(`Order ${clientOrderId} still PendingNew after 5s`);
}
Timestamps on rejected orders
Every order row - including rejections - carries:
| Field | Meaning |
|---|---|
startTime | When the order was submitted (ISO 8601 UTC). Use this as the rejection timestamp when reporting to users if the order never reached a working state. |
lastUpdated | When the row last changed. On async rejections, lastUpdated is a few milliseconds after startTime - that gap is when text and orderStatus: "Rejected" landed. |
Example R118 rejection (route omitted):
{
"orderStatus": "Rejected",
"text": "R118: Stop price is on wrong side of quote",
"startTime": "2026-01-15T16:49:58.5373894Z",
"lastUpdated": "2026-01-15T16:49:58.5412753Z"
}
Reading the text field
Rejection messages use two common formats:
-
R##:prefix - engine reason codes. Parse with/^(R\d+)/:const code = order.text?.match(/^(R\d+)/)?.[1]; // "R118"const message = order.text?.replace(/^R\d+:\s*/, ''); // "Stop price is on wrong side of quote" -
Venue rejection messages without an
R##prefix - for example:Rejected: Broker Rejected Limit price too far from NBBOTreat the full string as the reason; do not assume every rejection starts with
R##. -
null- the order is rejected but no reason string was surfaced. On R118 with an explicit route, this is the normal (unfortunate) POST/GET outcome — infer “stop on wrong side of quote” from order parameters, or scanGET /ordersfor theRejectedrow. Some POST responses also rewriteclientOrderIdwith anINVALIDsuffix; log the full POST body and useGET /orderswhenGET /orderreturns404.
On paper accounts, the simulator does not mirror live R118/R54/route-check behavior: invalid stops may fill, unsupported routes may be accepted, and text may carry a paper-only simulator placeholder instead of engine rejection codes. Rejection codes and timing on paper may differ from live accounts.
Do not cancel already-rejected orders
DELETE /orders/{clientOrderId} against a terminal Rejected order returns HTTP 200 (not 404), but the response overwrites the original rejection reason:
| Before cancel | After cancel |
|---|---|
"text": "R118: Stop price is on wrong side of quote" | "text": "R130: Cancel Request Rejected: Original order not found or cancelable..." |
R130 is a cancel failure, not the original placement rejection. If your UI only reads text after a cancel attempt, users will see R130 instead of R118/R95/etc.
Integration rule: when orderStatus === "Rejected", display text immediately and do not send cancel. Terminal orders (Rejected, Filled, Canceled, Expired) cannot be canceled.
Rejection code reference
Common codes on live accounts. Not exhaustive — the engine may return additional codes.
| Code | Example text | Typical trigger | POST timing |
|---|---|---|---|
| R24 | R24: Your order cannot have a STOP price of Zero | Stop / StopLimit submitted without a valid stopPrice | Async (PendingNew first) |
| R54 | R54: Unable to reach the destination Route | Bogus or unavailable route | Async |
| R78 | R78: Market orders are not allowed at this time | Market order outside Regular Trading Hours (9:30 AM–4:00 PM ET) | Sync when outside RTH; may fill during RTH |
| R95 | R95: Cannot have opening buy and sell orders at the same time | Opening long and opening short on the same symbol simultaneously | Async (~4 ms) |
| R06 | R06: This symbol is restricted from trading at this price | Limit far below/above allowed band for the symbol | Async |
| R114 | R114: Invalid duplicate UserOrderId | Reused clientOrderId in the same session | Sync on POST body; duplicate POST on live may also rewrite clientOrderId with INVALID suffix |
| R118 | R118: Stop price is on wrong side of quote | Stop trigger on the wrong side of the quote | Without route: async with full text. With route (SMART): sync — Rejected + text: null, clientOrderId rewritten with INVALID suffix, GET /order → 404 |
| R130 | R130: Cancel Request Rejected: … | Cancel attempted on a non-cancelable / terminal order | From DELETE, not POST |
| R145 | R145: Negative Price Is Not Allowed | Negative limitPrice or stopPrice | Sync |
| (none) | Rejected: Broker Rejected Limit price too far from NBBO | Limit price too far from the NBBO for the route | Async; GET /orders may show Rejected before GET /order |
| (none) | null | R118 with route, buying-power, or risk checks with no surfaced code | Sync on POST; row on GET /orders only |
For schema-level failures (bad enum, missing field, fractional quantity), see Validation errors - those return HTTP 400 plain text, not orderStatus: "Rejected".
Handling rejections in client code
const post = await res.json();
// Step 1: never stop at POST when status is PendingNew
const order =
post.orderStatus === 'PendingNew'
? await awaitOrderTerminal(accountId, post.clientOrderId)
: post;
// Step 2: act on terminal rejection
if (order.orderStatus === 'Rejected') {
const code = order.text?.match(/^(R\d+)/)?.[1] ?? null;
logRejection({
clientOrderId: order.clientOrderId,
code,
text: order.text ?? '(no reason surfaced)',
rejectedAt: order.lastUpdated ?? order.startTime,
});
// Do NOT call DELETE /orders/{cid} - see R130 overwrite above
return;
}
See also Handling R-code rejections and the Error handling template below.
Recommended production pattern
Every order placement path - manual tickets, algos, batch submitters - should follow the same outcome resolution:
- Generate a unique
clientOrderIdbeforePOST /order. - Have a listener ready - Portfolio WebSocket subscribed to
"Order"for the account before POST, or a poll loop onGET /order/{clientOrderId}and fallback toGET /orders. - POST the order and read the response, but do not treat
PendingNewas success. - Capture
post.clientOrderIdfrom the response — it may differ from the ID you sent (R118 routed rejections appendINVALID…). - Resolve the final state within ~5 seconds:
- WebSocket: wait for an
Orderupdate whereorderStatus === "Rejected"(or another terminal status) and readtext. - REST: poll
GET /order/{post.clientOrderId}every 50 ms until status is no longerPendingNew(retry on404). If still404, scanGET /ordersfor aRejectedrow with matchingsymbol/startTime.
- WebSocket: wait for an
- On
Rejected, persist and display:text(full reason string)- parsed R-code if present (
order.text?.match(/^(R\d+)/)?.[1]) lastUpdated(orstartTimeiflastUpdatedis absent) as the rejection timestampclientOrderId,symbol,side,orderType,route
- Do not call
DELETE /orders/{clientOrderId}on rejected orders - see R130 overwrite.
async function placeAndResolve(accountId: string, body: OrderRequest) {
const res = await fetch(`${BASE}/v1/api/accounts/${accountId}/order`, {
method: 'POST',
headers: AUTH_HEADERS,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const post = await res.json();
const final =
post.orderStatus === 'PendingNew'
? await awaitOrderTerminal(accountId, post.clientOrderId)
: post;
if (final.orderStatus === 'Rejected') {
return {
ok: false as const,
clientOrderId: final.clientOrderId,
code: final.text?.match(/^(R\d+)/)?.[1] ?? null,
reason: final.text ?? '(no reason surfaced)',
rejectedAt: final.lastUpdated ?? final.startTime,
};
}
return { ok: true as const, order: final };
}
Troubleshooting: "rejection not received over the API"
| Symptom | Cause | Fix |
|---|---|---|
POST returned PendingNew, no error shown | Client stopped at POST body; rejection is async | Poll GET /order/{post.clientOrderId} or listen on Portfolio WS |
POST returned Rejected + text: null, GET → 404 | R118 with route (routed stop on wrong side) — server rewrote clientOrderId | Use GET /orders; do not cancel; infer R118 from order params |
GET /order/{sentId} always 404 after POST | POST response used a different clientOrderId (…INVALID… suffix) | Poll with post.clientOrderId; fall back to GET /orders |
| User saw error only after cancel attempt | Cancel on Rejected order overwrote text with R130 | Read rejection before cancel; never cancel terminal orders |
GET /order/{cid} returned 404 once, client gave up | Registration race right after POST | Retry for 1–2 s before treating as missing |
GET /orders shows Rejected but GET /order still New | Async rejection appeared in GET /orders before GET /order updated | Keep polling GET /order or trust GET /orders row |
| No WebSocket rejection event | WS connected after POST, or not subscribed to "Order" | Open WS and subscribe before placing; buffer messages during REST bootstrap |
Only saw PendingNew on WebSocket | Ignored the second push | Async rejections send two WS updates - wait for Rejected |
| Reason missing from historical archive | GET /orders/start-date is fill-level only | Use GET /orders (today's book) or your own rejection log keyed on clientOrderId |
Validation errors
There are two distinct classes of rejection to handle:
HTTP 400 - schema or body parse error. Returned when the request doesn't parse or a field fails JSON Schema validation before routing. The body is plain text (Content-Type: text/plain). Read res.text() and branch on the Content-Type header before parsing.
| Condition | 400 plain-text body |
|---|---|
symbol missing or contains characters outside ^[a-zA-Z0-9._-]+$ (including leading or trailing whitespace) | - symbol: Does not match pattern '^[a-zA-Z0-9._-]+$' |
| Symbol contains an unescaped tab or newline inside the JSON string literal | invalid character '\t' in string literal |
clientOrderId contains an unescaped control character (tab, newline, CR) | invalid character '\t' in string literal |
clientOrderId contains an unescaped double-quote or single-quote | invalid character 'w' after object key:value pair (or similar JSON parse error) |
side missing | - (root): side is required |
side not "Buy" or "Sell" | - side: side must be one of the following: "Buy", "Sell" |
orderType missing or not a valid value | - orderType: orderType must be one of the following: "Limit", "Market", "Stop", "StopLimit" |
securityType missing or not a valid value | - securityType: securityType must be one of the following: "Stock", "Option", "Mleg" |
timeInForce missing or not a valid value | - timeInForce: timeInForce must be one of the following: "Day", "GoodTillCancel", "AtTheOpening", "ImmediateOrCancel", "FillOrKill", "GoodTillCrossing", "Day_Plus", "GTC_Plus" |
orderQuantity is a non-integer number (e.g. 0.5) | - orderQuantity: Invalid type. Expected: integer, given: number |
orderQuantity missing / body not parseable | invalid character '<' looking for beginning of value (or similar parse error) |
orderQuantity < 1 | - orderQuantity: Must be greater than or equal to 1 |
orderQuantity > 1,000,000 | - orderQuantity: Must be less than or equal to 1e+06 |
| Empty body | invalid character '<' looking for beginning of value |
| Form-urlencoded or plain-text body | 404 Not found |
For side, orderType, securityType, and timeInForce, wrong case (e.g. "buy", "LIMIT") returns 400. For openClose, send "Open" or "Close" with that exact casing in production code - some environments reject wrong case with orderStatus: "Rejected", while others may normalize it.
HTTP 200 with orderStatus: "Rejected". Some valid-looking requests are declined at routing time. The response is still HTTP 200 - read orderStatus and text to determine the outcome. On live routed orders, POST /order may return PendingNew first; poll GET /order/{clientOrderId} for the final reason. See Order rejections.
text field value | Condition |
|---|---|
"R24: Your order cannot have a STOP price of Zero" | Stop / StopLimit without a valid stopPrice |
"R54: Unable to reach the destination Route" | Invalid or unreachable route |
"R78: Market orders are not allowed at this time" | Market order submitted outside Regular Trading Hours |
"R95: Cannot have opening buy and sell orders at the same time" | Attempted to open a long and a short on the same symbol simultaneously |
"R114: Invalid duplicate UserOrderId" | clientOrderId was already used in this session |
"R118: Stop price is on wrong side of quote" | Stop trigger on the wrong side of the current quote |
"R130: Cancel Request Rejected: …" | Cancel attempted on a non-cancelable order (from DELETE, not placement) |
"R145: Negative Price Is Not Allowed" | Negative limitPrice or stopPrice |
"Rejected: Broker Rejected …" | Venue declined the order (no R## prefix; legacy text format) |
null | Rejected without a surfaced reason - inspect account state; see Order rejections |
HTTP 400 with JSON body - cancel-by-id account mismatch. When DELETE /orders/{clientOrderId} is sent against an account ID that does not match the keys you authenticated with, the API returns 400 with a JSON body:
{
"statusCode": "BadRequest",
"message": "CancelOrderWithResponse",
"detail": "Unable to fetch account orders from server. Account for User was not found, or User doesn't have entitlements."
}
This is distinct from the 404 Not found plain-text response, returned when the order was not found (wrong clientOrderId, order not yet registered, order already terminal, or auth headers that do not grant access).
Sending additional JSON fields in the POST /order body (e.g. myCustomTag, strategy, notes) does not cause an error - only fields defined in the schema are processed; extra fields are ignored.
The server accepts StopLimit orders without checking that stopPrice and limitPrice are coherent for the direction of the order. A combination like Buy StopLimit with stopPrice: 100, limitPrice: 50 is accepted and placed, but the order will sit unfilled because the limit price is unreachable once the stop triggers. Check price coherence in your client before submitting a StopLimit order.
Order identity
clientOrderId
clientOrderId is the recommended way to identify your orders. The server echoes the exact value you sent in POST /order, GET /order/{clientOrderId}, and GET /orders responses, and accepts it in the path of DELETE /v1/api/accounts/{accountId}/orders/{orderId} (where {orderId} is your clientOrderId) to cancel an order. The same field is used for deduplication.
On Portfolio WebSocket Order pushes, the id appears as the suffix of userOrderId ("{accountId}:{clientOrderId}") rather than as a top-level clientOrderId - split on the first : to recover the bare id. See Order shape across REST and WebSocket.
On live accounts, keep clientOrderId ≤ 36 characters for reliable cancel-by-id at the venue. Paper accounts accept longer values. UUIDs without dashes are 32 characters; with dashes, 36.
Duplicate detection
The server enforces uniqueness on clientOrderId per session. Submitting the same clientOrderId a second time returns HTTP 200 with "orderStatus": "Rejected" and "text": "R114: Invalid duplicate UserOrderId". The deduplication check also applies to concurrent submissions - if two requests carrying the same clientOrderId arrive simultaneously, exactly one is accepted and the rest are rejected with R114. Always generate a fresh unique value for each new order.
clientOrderId cannot be reused after cancellationA clientOrderId is consumed permanently the moment it is accepted - even after the order is fully canceled or rejected, the same value cannot be sent again on a new POST /order. Re-submitting the same id will return R114. To "modify" an order, generate a new clientOrderId for the replacement (see Modifying an order). Treat clientOrderId like a UUID: one value, one order, forever.
clientOrderId character set
The clientOrderId field has no declared character-set constraint in the schema, but certain characters cause problems at the JSON layer:
| Character class | Behavior |
|---|---|
| Alphanumerics, hyphens, underscores | Safe - recommended |
Colons (:) | Accepted by the server |
Forward slashes (/) | Accepted by the server |
| Spaces | Accepted by the server (though use with care in URL paths) |
| Unicode (non-ASCII, including emoji) | Accepted - the server handles UTF-8 |
Unescaped control characters (tab \t, newline \n, CR \r) | 400 - causes a JSON parse error |
Unescaped quote characters (" or ') | 400 - breaks the JSON string literal |
Null byte (\0) | 400 - causes a JSON parse error |
Use URL-safe alphanumerics and hyphens (e.g. UUIDs) for maximum portability.
Auto-generated clientOrderId
When you omit clientOrderId entirely or send clientOrderId: "" (an explicit empty string), the server generates a numeric clientOrderId in the format MMDDHHmmssSSS.NNNN - for example, 0513160835877.1906. This is constructed from the order timestamp. The generated ID is echoed back in the POST /order response and on GET /orders / GET /order/{clientOrderId} rows. It is usable for cancel-by-id.
clientOrderId: null does not auto-generateSending clientOrderId: null explicitly is different from omitting the field. Omit the field or send a non-empty string to receive an auto-generated ID usable for cancel-by-id. With null, the response echoes clientOrderId: "<no value>".
Opening buy + sell on the same symbol
Placing both an opening Buy and an opening Sell (short) on the same symbol at the same time is not permitted. The second order returns HTTP 200 with "orderStatus": "Rejected" and "text": "R95: Cannot have opening buy and sell orders at the same time".
{
"orderStatus": "Rejected",
"text": "R95: Cannot have opening buy and sell orders at the same time",
"clientOrderId": "my-second-order",
"canceledQuantity": 1,
"executed": 0,
"leavesQuantity": 0
}
Order lifecycle
A limit order moves through this lifecycle:
POST /order → orderStatus: "PendingNew" (accepted; routing in progress)
↓ (milliseconds)
GET /orders → orderStatus: "New" (acknowledged at venue, resting in book)
↓ (seconds to hours depending on market conditions)
GET /orders → orderStatus: "Filled" (fully executed)
Common orderStatus values (not exhaustive - additional statuses exist for partial fills, pending replaces, and suspended orders):
orderStatus | Terminal? | Meaning |
|---|---|---|
PendingNew | No | Gateway has accepted the order; routing in progress. Transitions to New within milliseconds. |
Accepted | No | Order acknowledged by the venue (returned on Portfolio WebSocket pushes). Functionally equivalent to New for blotter purposes - treat both as working. |
New | No | Order is resting in the order book at the venue, waiting to be filled. |
PartiallyFilled | No | Some shares have filled; order is still working for the remainder. |
Filled | Yes | Fully executed. executed equals orderQuantity, leavesQuantity is 0. |
PendingCancel | No | Cancel request received; awaiting confirmation from venue. |
Canceled | Yes | Successfully canceled. canceledQuantity is set to the canceled-out shares. |
Rejected | Yes | Declined. Read text for the R-code reason. canceledQuantity carries the unfilled quantity. |
DoneForDay | Semi | Order expired at end of session. For GoodTillCancel and GTC_Plus orders it will resurface the next session; for all others it is effectively terminal. |
Expired | Yes | Order expired (e.g. OPG order not filled at the open). |
To cancel a resting order:
DELETE /orders/{clientOrderId} → 200 OK (cancel request sent)
↓
GET /orders → orderStatus: "Canceled"
Polling cadence: After placing an order, poll GET /order/{clientOrderId} every 50 ms until the status leaves PendingNew - rejections on live routed orders land within ~50 ms. When listing the full book, a 1–2 second delay before GET /orders is enough for new rows to appear. For real-time outcomes without polling, subscribe to the Portfolio WebSocket stream, which pushes order state changes (PendingNew, Accepted, Filled, Canceled, Rejected, etc.) as they occur. See Order rejections.
Terminal statuses: Filled, Canceled, Rejected, Expired. An order in a terminal state must not be canceled - DELETE /orders/{clientOrderId} on a Rejected order returns HTTP 200 but overwrites text with R130, masking the original rejection reason. Other terminal orders may return 404 Not found from cancel. DoneForDay is semi-terminal: GTC/GTC+ orders may resurface the next session; Day orders in DoneForDay are effectively terminal.
Modifying an order
There is no PUT or PATCH endpoint for orders. To change the price, quantity, time-in-force, or any other field of a working order, follow the cancel-then-replace pattern:
DELETE /v1/api/accounts/{accountId}/orders/{originalClientOrderId}- cancel the working order.- Wait for the cancellation to settle. Poll
GET /ordersuntil the original order'sorderStatusisCanceled. POST /v1/api/accounts/{accountId}/order- submit a fresh order with a newclientOrderIdand the updated fields.
async function modifyOrder(
accountId: string,
originalClientOrderId: string,
updates: Partial<OrderRequest>,
): Promise<OrderResponse> {
// 1. Cancel the working order
await fetch(
`/v1/api/accounts/${accountId}/orders/${encodeURIComponent(originalClientOrderId)}`,
{ method: 'DELETE', headers: authHeaders() },
);
// 2. Brief settle so the cancel propagates
await new Promise((r) => setTimeout(r, 400));
// 3. Re-submit with a NEW clientOrderId. Reusing the original id will be
// rejected with R114 even though the original order is canceled.
const newId = `${originalClientOrderId}-r${Date.now()}`;
const replacement = {
...originalRequest, // your stored copy of the original POST body
...updates,
clientOrderId: newId,
};
const res = await fetch(`/v1/api/accounts/${accountId}/order`, {
method: 'POST',
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(replacement),
});
return res.json();
}
clientOrderId?A clientOrderId is consumed at the moment it's accepted, regardless of where the order ends up. Reusing the same id on a replacement order - even after a clean cancellation - returns R114 (Invalid duplicate UserOrderId). Generate a new id for every POST /order.
Between steps 1 and 3 there is a brief window in which a partial fill on the original order is still possible. If your strategy cannot tolerate a partial fill on the original price, place the replacement only after GET /orders confirms the original is in the Canceled (or fully Filled) terminal state.
Common workflows
Place a limit buy with a stop-loss
When the trader has chosen their entry and stop levels, your application can enter the long position with a limit order and then submit the trader's stop-loss once the entry fills. The key rule: poll until the entry is in Filled state before placing the protective stop - placing the stop-loss while the entry is still PendingNew may result in a dangling stop if the entry never fills.
// 1. Buy 100 AAPL at limit
const entry = await placeOrder({
securityType: 'Stock', symbol: 'AAPL',
side: 'Buy', openClose: 'Open',
orderType: 'Limit', limitPrice: 195.00,
orderQuantity: 100, timeInForce: 'Day',
clientOrderId: 'entry-aapl-001',
});
// Reject if the placement itself failed at routing time
if (entry.orderStatus === 'Rejected') {
throw new Error(`Entry order rejected: ${entry.text}`);
}
// 2. Poll until the entry order fills, then attach a stop-loss.
// The Portfolio WebSocket stream is more efficient for production use.
let filled = false;
for (let i = 0; i < 30 && !filled; i++) {
await sleep(2000);
const orders = await getOrders(); // GET /orders → match by clientOrderId
const e = orders.find(o => o.clientOrderId === 'entry-aapl-001');
filled = e?.orderStatus === 'Filled';
}
if (filled) {
await placeOrder({
securityType: 'Stock', symbol: 'AAPL',
side: 'Sell', openClose: 'Close',
orderType: 'Stop', stopPrice: 190.00,
orderQuantity: 100, timeInForce: 'GoodTillCancel',
clientOrderId: 'stop-aapl-001',
});
}
For a position-aware variant that reads priceAvg off /positions and sizes the stop at a configurable percentage, see the Stop-Loss on a Long Position recipe.
Short with pre-borrow check
Before placing a short order, confirm the symbol is easy to borrow. Hard-to-borrow symbols require a locate reservation through the Short Locates workflow before the short can be placed.
// 1. Check borrow availability
const etb = await fetch(
`/v1/api/accounts/${accountId}/is-easy-to-borrow/symbol/TSLA`,
{ headers }
).then(r => r.json());
if (!etb.isEasyToBorrow) {
throw new Error('TSLA is not easy to borrow - use Short Locates to reserve shares');
}
// 2. Place short
await placeOrder({
securityType: 'Stock', symbol: 'TSLA',
side: 'Sell', openClose: 'Open',
orderType: 'Limit', limitPrice: 350.00,
orderQuantity: 50, timeInForce: 'Day',
clientOrderId: 'short-tsla-001',
});
When the symbol is hard-to-borrow, the Locate & Sell Short recipe shows the full quote → accept → short flow against a reserved locate.
End-of-day cleanup
Cancel all open orders and close any remaining positions before the session ends. Always cancel first - if you close positions before canceling, the working orders may fill and reopen what you just closed.
// Cancel all open orders, then flatten any residual positions
const form = new FormData();
form.append('account', accountId);
await fetch('https://webapi.trade0.click/v1/api/accounts/orders', {
method: 'DELETE',
headers: { 'TZ-API-KEY-ID': key, 'TZ-API-SECRET-KEY': secret },
body: form,
});
For the full cancel-then-flatten workflow with verification and per-order fallback, see the Cancel All Open Orders and Flatten All Positions recipes.
Handling R-code rejections
Routing-time rejections arrive as HTTP 200 with orderStatus: "Rejected". Extract the R-code from the text field and dispatch accordingly:
const data = await res.json();
if (data.orderStatus === 'Rejected') {
const code = data.text?.match(/^(R\d+)/)?.[1];
switch (code) {
case 'R78':
throw new Error('Market orders are not permitted outside Regular Trading Hours. Use a Limit order with Day_Plus or GTC_Plus instead.');
case 'R95':
throw new Error('Cannot open both a long and a short on the same symbol simultaneously. Cancel the existing open order first.');
case 'R114':
// Note: a clientOrderId is consumed permanently - even after a clean
// cancellation the same id cannot be reused. Always generate a new id
// for every POST /order, including replacements / modifications.
throw new Error('Duplicate clientOrderId. Generate a new unique ID for each order (ids are not reusable after cancellation).');
case 'R118':
throw new Error('Stop price is on the wrong side of the current quote for this order direction.');
case 'R130':
// From DELETE /orders/{id} on a terminal order - not a placement rejection.
throw new Error('Cancel rejected - order is already terminal. Read text before canceling.');
default:
console.warn('Order rejected:', data.text ?? '(no reason)');
}
}
Error handling
Every POST /order call requires two levels of error checking:
- HTTP-level -
!res.okcatches400(schema error, plain-text body),401(cancel endpoint auth),404(auth/account on GET and POST), and405(wrong method). - Application-level - the HTTP status can be
200while the order is still rejected. Always readorderStatusafter parsing.
const res = await fetch(url, { method: 'POST', body: JSON.stringify(order), headers });
if (!res.ok) {
// 400 = schema validation error (plain text body - branch on Content-Type)
// or account mismatch on POST/DELETE (JSON body: {statusCode, message, detail})
// 401 = missing auth on DELETE /orders/{id} (JSON body: {statusCode, message})
// 404 = auth failure or unknown account on GET / POST
// 405 = wrong HTTP method
const contentType = res.headers.get('content-type') ?? '';
const body = contentType.includes('application/json')
? await res.json()
: await res.text();
throw new Error(`HTTP ${res.status}: ${JSON.stringify(body)}`);
}
const data = await res.json();
if (data.orderStatus === 'Rejected') {
// Routing-time rejection. data.text carries the R-code, or null for unlabeled rejections.
console.warn('Order rejected:', data.text ?? '(no reason code)');
}
FAQs
Why didn't my order rejection show up on the POST response?
Many live routed orders reject asynchronously. POST /order returns orderStatus: "PendingNew" and text: null; the reason (for example R118) appears on GET /order/{clientOrderId} within ~50 ms. Always poll single-order lookup or subscribe to the Portfolio WebSocket after placement. See Order rejections.
Why did canceling show a different rejection message (R130)?
Canceling an already-Rejected order returns HTTP 200 but overwrites the original text with R130: Cancel Request Rejected: …. Display the placement rejection before canceling, and do not cancel terminal orders. See Do not cancel already-rejected orders.
Where are rejection timestamps?
Use startTime for when the order was submitted and lastUpdated for when the rejection landed (on async rejections, lastUpdated is a few milliseconds after startTime).
Why does a market order return 200 OK but orderStatus: "Rejected"?
Order placement returns HTTP 200 once the request body is structurally valid. Whether the order is accepted for routing is reflected in orderStatus. Market orders outside regular trading hours (before 9:30 AM or after 4:00 PM ET) return orderStatus: "Rejected".
Can I modify a working order?
There is no modify endpoint. Use the cancel-then-replace pattern: DELETE /orders/{id} the original, wait for the cancel to settle, and POST /order a replacement with a fresh clientOrderId. Reusing the original id even after the order is canceled returns R114 - the id is consumed permanently the moment it is first accepted.
Which field should I use to identify my orders?
Use clientOrderId. It is echoed back verbatim in the POST /order, GET /order/{cid}, and GET /orders responses, and is the value you pass to cancel-by-id. On Portfolio WebSocket Order pushes, split the suffix from userOrderId ("{accountId}:{clientOrderId}") to recover it. Keep it ≤ 36 characters for live-exchange compatibility.
Why does canceling an order return 404, 400, or 401?
DELETE /orders/{id} has three failure shapes. A 401 Unauthorized (JSON body: {"statusCode":"Unauthorized","message":"Token not provided","detail":null}) means auth headers were missing. A 404 Not found (plain text) means the order was not found - wrong ID, not yet registered, already terminal (Filled, Canceled, Expired), or auth headers that do not grant access. Rejected orders may return HTTP 200 from cancel with R130 in text instead of 404 - never cancel them. A 400 Bad Request (JSON body with "message":"CancelOrderWithResponse") means the path account ID does not match the keys you authenticated with. In all cases, confirm the final orderStatus via GET /orders.
What does the R95 error mean?
"R95: Cannot have opening buy and sell orders at the same time" is returned when you try to place both an opening long and an opening short on the same symbol simultaneously. Cancel one direction before placing the other, or use openClose: "Close" to indicate you are closing an existing position rather than opening a new one.
Does cancel-all work even when there are no open orders?
Yes. DELETE /v1/api/accounts/orders returns {"message":"Cancel Request Submitted Successfully"} regardless of whether any orders were open. The request is always accepted as long as the multipart body carries a valid account field.
Are enum values case-sensitive?
Yes. For side, orderType, securityType, and timeInForce, wrong case returns 400. For openClose, always send "Open" or "Close" exactly - wrong case returns HTTP 200 with orderStatus: "Rejected".
Are ticker symbols case-sensitive?
The pattern ^[a-zA-Z0-9._-]+$ accepts any case, but the server stores and routes the symbol verbatim - it does not uppercase. To match how exchanges and market-data feeds canonicalize tickers, always send the uppercase form ("AAPL", never "aapl").
Can I send fractional shares?
No. orderQuantity must be an integer. A non-integer value (e.g. 0.5) returns 400 Bad Request with - orderQuantity: Invalid type. Expected: integer, given: number.
How many decimals can limitPrice have?
The JSON validator accepts up to four decimals. To stay compliant with SEC Rule 612, send two decimals for prices at or above $1.00 and up to four decimals for sub-$1.00 prices. Sending 250.0001 on a $250 stock will be accepted by the schema but will not be honored by the venue.
Is there a rate limit on POST /order?
Yes. The Trading API enforces per-endpoint limits per API key. POST /order allows 10/s, while read endpoints throttle sooner. See API Rate Limits for the full table, 429 behavior, and backoff guidance.
Can I cancel an order immediately after placing it?
A DELETE /orders/{id} issued before the order is registered may return 404 Not found. Poll GET /orders until the order appears before canceling, or retry the cancel after a brief delay.
What is route: "<no value>" in a rejected order response?
For some validation-time rejections, the route field can surface as the literal string "<no value>" instead of a real route name. Most rejected orders retain the route they were submitted on - for example, a rejected paper order returns "route": "PAPER". Check orderStatus === "Rejected" and read text for the reason code (e.g. R88, R145).
Building a complete order management integration
The sections below cover the integration patterns you need once you move beyond individual order calls - response normalization, live order books, position-aware sizing, extended-hours handling, and portfolio liquidation.
Order shape across REST and WebSocket
POST /order, GET /order/{clientOrderId}, GET /orders (each row), and the Portfolio WebSocket Order push all describe the same conceptual object - an order - but WebSocket (and some filled Mleg lookups) use different wire field names than REST.
| Shape | Endpoints | Account-id field | Client-id field | Quantity fields |
|---|---|---|---|---|
| Clean | POST /order; GET /order/{clientOrderId}; GET /orders (each row); Mleg GET /order/{cid} before legs[] is populated | accountId | clientOrderId (bare) | canceledQuantity, lastQuantity, leavesQuantity, maxDisplayQuantity |
| WebSocket (enriched) | Portfolio WebSocket Order push; GET /order/{clientOrderId} for Mleg orders once legs[] is populated | account | userOrderId = "{accountId}:{clientOrderId}" (split on :) | cancelledQuantity (double l), lastQty, leavesQuantity (also lvsQty), maxDisplayQty |
The WebSocket shape carries extra fields the clean REST shape doesn't (accountType, condition, legIndex, mlegID, startTimeET, status alias of orderStatus, …). The values that overlap match between the two shapes; only the keys differ.
Practical rules:
- Write a small normalizer that maps the WebSocket shape onto the clean shape (
account→accountId, splituserOrderIdintoaccountId+clientOrderId,cancelledQuantity→canceledQuantity,lastQty→lastQuantity,maxDisplayQty→maxDisplayQuantity). Run it on every Portfolio WebSocketOrdermessage and everyGET /order/{cid}response that has a non-nulllegs[]. REST responses fromPOST /order,GET /orders, and mostGET /order/{cid}calls are already in the clean shape. - Pick
clientOrderIdas your stable join key. After normalization it's identical across all four sources, and the same value you sent onPOST /order. - Seed your local cache from
GET /orderson startup (the today's-orders list including any working multi-day GTCs), then apply Portfolio WebSocket pushes to keep it live. TreatorderStatus: "Filled","Canceled","Rejected","Expired", or"DoneForDay"as terminal. - The
sidefield onGET /ordersrows is enriched for short/cover trades - see Thesidefield - enriched values below. WebSocket pushes carry the raw"Buy"/"Sell"you sent. If you display labels from WS messages, derive them yourself fromside+openClose. textcarries the rejection reason forRejectedrows ("R##: …") and isnullfor accepted rows; on paper, the simulator may set a placeholder string intext. Checktextexplicitly when you act on rejection codes.
The side field - enriched values in order history
When the server returns order rows from GET /orders, the side field carries an enriched value that reflects the trader action - not just the raw "Buy" or "Sell" you sent. This lets display layers show the correct label without re-deriving it from openClose.
side in request | openClose in request | side in GET /orders response | Trader-facing label |
|---|---|---|---|
"Buy" | "Open" | "Buy" | Buy |
"Sell" | "Close" | "Sell" | Sell |
"Sell" | "Open" | "SellShort" | Short |
"Buy" | "Close" | "Buy" (Cover orders return raw "Buy" - derive the Cover label client-side) | Cover |
Enrichment is a GET /orders read-only annotation. A POST /order response with side: "Buy", openClose: "Close" carries "side": "Buy" - not "BuyToCover". WebSocket pushes also carry the raw wire value. To label Cover orders reliably, derive the label client-side from side: "Buy" + openClose: "Close" rather than expecting a "BuyToCover" enrichment in the response.
Never send "SellShort" or "BuyToCover" in a POST /order request - the schema rejects any value other than "Buy" or "Sell" with a 400 error.
Building a live order book - and pairing it with execution history
GET /orders returns working and session-visible orders - New, Accepted, PartiallyFilled, and other non-terminal states, plus any still-working orders carried over from previous sessions (multi-day GoodTillCancel / GTC_Plus that haven't filled or expired). Recently filled or canceled orders may appear briefly; filled market orders can drop out quickly. To get a "still working right now" blotter, filter the response to the working states:
const WORKING = new Set(['New', 'PendingNew', 'Accepted', 'PartiallyFilled'])
const HARD_TERMINAL = new Set(['Filled', 'Canceled', 'Rejected', 'Expired', 'DoneForDay'])
async function getWorkingOrderBook(): Promise<TZOrder[]> {
const all = await fetchOrders() // GET /orders → orders[]
// Note: DoneForDay rows on GTC TIFs may resurface next session; drop them
// here for a "still working right now" view.
return all.filter((o) => WORKING.has(o.orderStatus))
}
The companion endpoint, GET /orders/start-date/{date}, returns historical orders - the post-trade record at the fill level, with different fields (tradeId, qty, price, commission, totalFees, grossProceeds, netProceeds, tradeDate). It exists to support post-trade reconciliation, P&L recaps, fee/commission audits, and compliance exports, and supports up to one week of history per request. Combine it with GET /orders when you need a full picture of recent account activity:
const weekAgo = new Date(Date.now() - 7 * 86_400_000)
.toISOString().slice(0, 10) // YYYY-MM-DD
const [working, history] = await Promise.all([
getWorkingOrderBook(), // current intent
fetchHistoricalOrders(weekAgo) as Promise<TZHistoricalOrderRow[]>, // past activity
])
// `working`: order rows you can still cancel / amend.
// `history`: immutable per-fill rows (indexed by tradeId).
To correlate a historical-orders row back to its originating order, match on symbol + tradeDate + side + qty + price - these rows do not carry clientOrderId. See the End-of-Day Recap recipe for a working historical-orders example.
Which actions are available given a position
The available trader actions for a symbol depend on whether the account holds a position and whether the symbol is easy to borrow. This is not enforced by the API - it is UI-level logic, but understanding it prevents placing orders the API will reject:
type TraderAction = 'Buy' | 'Short' | 'Sell' | 'Cover' | 'Locate'
function getAvailableActions(
positionSide: 'long' | 'short' | 'none',
isEasyToBorrow: boolean | null,
hasLocates: boolean,
): TraderAction[] {
if (positionSide === 'long') return ['Buy', 'Sell']
if (positionSide === 'short') return ['Short', 'Cover']
if (isEasyToBorrow === true || hasLocates) return ['Buy', 'Short']
if (isEasyToBorrow === false) return ['Buy', 'Locate'] // needs a locate before shorting
return ['Buy', 'Short']
}
Extended-hours order coercion
Outside Regular Trading Hours (before 9:30 AM or after 4:00 PM ET), Market orders are rejected. Your client should detect extended hours and coerce:
Market→Limitat the last traded price (or mid-point of bid/ask)Day→Day_PlusGTC/GTC+→GTC_Plus
function mapTif(tif: string, extendedHours: boolean): TZTimeInForce {
if (extendedHours) {
if (tif === 'GTC' || tif === 'GTC+') return 'GTC_Plus'
return 'Day_Plus' // everything else becomes Day_Plus in extended hours
}
switch (tif) {
case 'Day': return 'Day'
case 'Day+': return 'Day_Plus'
case 'GTC': case 'GTC+': return 'GoodTillCancel'
default: return 'Day'
}
}
async function placeWithExtendedHoursCoercion(
order: TZPlaceOrderRequest,
lastPrice: number,
): Promise<TZPlaceOrderRequest> {
const extended = isExtendedHours()
if (!extended) return order
return {
...order,
orderType: order.orderType === 'Market' ? 'Limit' : order.orderType,
timeInForce: mapTif(order.timeInForce, true),
// Only set limitPrice if we coerced Market->Limit and it wasn't already set
...(order.orderType === 'Market' && !order.limitPrice
? { limitPrice: lastPrice }
: {}),
}
}
Position-aware order sizing
Client applications can size orders dynamically rather than using a fixed share count. Common sizing approaches:
| Approach | Calculation |
|---|---|
| Fixed shares | quantity = config.shares |
| Dollar amount | quantity = Math.floor(dollarAmount / lastPrice) |
| % of buying power | quantity = Math.floor((buyingPower * pct) / lastPrice) |
| % of current position | quantity = Math.round(Math.abs(position.shares) * pct) |
| Risk-based (dollar risk) | quantity = Math.floor(riskAmount / stopDistance) |
| Risk-based (% of equity) | quantity = Math.floor((equity * pct) / stopDistance) |
Always enforce a minimum of 1 share and a maximum of 1,000,000 (the API's orderQuantity ceiling). For position-based sizing, validate that you are sizing in the correct direction - a Sell order should check shares > 0, a Cover should check shares < 0.
Resolving the limit price from a quote
Rather than requiring a trader to type in a limit price each time, derive it from the current quote using rules the trader configures. Pick a price source and apply an optional offset (dollar or percentage): a Buy uses ask + offset; a Sell uses bid - offset; stop orders derive stopPrice from lowOfDay (longs) or highOfDay (shorts).
Common price sources:
| Source | Description |
|---|---|
bid | National best bid |
ask | National best ask |
last | Last traded price |
midpoint | (bid + ask) / 2 |
highOfDay | Today's session high |
lowOfDay | Today's session low |
avgEntry | The priceAvg of the current open position on this symbol |
type PriceSource = 'bid' | 'ask' | 'last' | 'midpoint'
| 'highOfDay' | 'lowOfDay' | 'avgEntry'
interface Quote {
bid: number; ask: number; price: number;
high: number; low: number;
}
function resolveLimitPrice(
source: PriceSource | undefined,
offset: number | undefined, // 0.05 = 5 cents (or 5%)
offsetType: 'dollar' | 'percent' | undefined,
quote: Quote | null,
position?: { priceAvg: number },
): number | undefined {
if (!source || !quote) return undefined
let base: number | undefined
switch (source) {
case 'bid': base = quote.bid; break
case 'ask': base = quote.ask; break
case 'last': base = quote.price; break
case 'midpoint': base = (quote.bid + quote.ask) / 2; break
case 'highOfDay': base = quote.high; break
case 'lowOfDay': base = quote.low; break
case 'avgEntry': base = position?.priceAvg; break
}
if (base === undefined || base <= 0) return undefined
const off = offset ?? 0
if (off === 0) return base
return offsetType === 'percent' ? base * (1 + off / 100) : base + off
}
Cancel filters - by side, by symbol, first, last
The API provides two cancel primitives: cancel a specific order by ID, or cancel all open orders (optionally scoped to a symbol). More targeted patterns - cancel all buy-side orders, cancel only shorts, cancel the most recent order - are built client-side by fetching the open order list and issuing individual cancels:
async function cancelMatching(
predicate: (o: TZOrder) => boolean,
): Promise<void> {
const orders = await getOrders() // GET /orders
const targets = orders.filter(predicate)
for (const o of targets) {
await cancelOrder(o.clientOrderId) // DELETE /orders/{clientOrderId}
}
}
// Cancel all open Buy orders
await cancelMatching(o => o.side === 'Buy' && !isTerminal(o.orderStatus))
// Cancel all open Sells on AAPL
await cancelMatching(o =>
o.symbol === 'AAPL' && o.side === 'Sell' && !isTerminal(o.orderStatus))
// Cancel all opening shorts (semantic side from /orders is "SellShort")
await cancelMatching(o => o.side === 'SellShort' && !isTerminal(o.orderStatus))
// Cancel all covering orders. Cover orders return raw `side: "Buy"`,
// so derive Cover semantics from side + openClose instead of relying on `side`.
await cancelMatching(o =>
o.side === 'Buy' && o.openClose === 'Close' && !isTerminal(o.orderStatus))
// Cancel the first open order on a symbol
const open = (await getOrders())
.filter(o => o.symbol === 'AAPL' && !isTerminal(o.orderStatus))
if (open.length) await cancelOrder(open[0].clientOrderId)
// Cancel the most recent open order on a symbol
if (open.length) await cancelOrder(open[open.length - 1].clientOrderId)
function isTerminal(s: string | undefined): boolean {
// Hard terminals - order cannot be canceled and will not resurface.
// Note: DoneForDay is semi-terminal (Day TIF = effectively terminal,
// GTC / GTC_Plus = may resurface next session). For cancel-eligibility
// checks, treat DoneForDay as terminal too - it can't be canceled either way.
return s === 'Filled' || s === 'Canceled' || s === 'Rejected'
|| s === 'Expired' || s === 'DoneForDay'
}
For symbol-scoped cancel-all, prefer the API's native ?symbol= parameter - it's atomic and avoids the race window between GET /orders and DELETE /orders/{id}:
// Native symbol-scoped cancel - single round-trip
const form = new FormData()
form.append('account', accountId)
await fetch(`/v1/api/accounts/orders?symbol=AAPL`, {
method: 'DELETE',
headers: { 'TZ-API-KEY-ID': key, 'TZ-API-SECRET-KEY': secret },
body: form,
})
Liquidation workflow - flatten everything
To flatten all open positions cleanly, cancel all working orders first (so they can't fill into the position you're trying to close), then issue a closing order for each non-zero position:
async function liquidateAll(): Promise<void> {
// 1. Cancel all open orders first - otherwise they may fill into
// a position you're trying to close, creating an overshoot.
await cancelAllOrders()
// 2. Read current positions
const positions = await getPositions()
const extended = isExtendedHours()
for (const pos of positions) {
if (!pos.shares || pos.shares === 0) continue
const action = pos.shares > 0 ? 'Sell' : 'Cover'
const { side, openClose } = resolveOrderAction(action)
const qty = Math.abs(pos.shares)
// For options, the position row has the underlying ticker in `symbol`
// and the OCC contract in `tradedSymbol`. POST /order on a single-leg
// option needs the OCC string, so fall back to tradedSymbol when present.
const orderSymbol = pos.tradedSymbol ?? pos.symbol
await placeOrder({
securityType: pos.securityType as 'Stock' | 'Option',
symbol: orderSymbol,
side,
openClose,
orderQuantity: qty,
// Use Market during RTH for guaranteed fill;
// coerce to Limit at last price during extended hours.
// Omitting limitPrice on a Limit order sets it to 0, which the venue rejects.
// pos.priceAvg is the average cost basis; substitute a live quote
// from your market-data feed for tighter fills.
orderType: extended ? 'Limit' : 'Market',
...(extended && { limitPrice: pos.priceAvg }),
timeInForce: extended ? 'Day_Plus' : 'Day',
})
}
}
Per-symbol liquidation workflow
Closing a single symbol's position mirrors the full-account liquidation but scopes both the cancel and the close to one symbol:
async function liquidateSymbol(symbol: string): Promise<void> {
// 1. Cancel any working orders on this symbol so they can't fill
// into the position you're trying to close.
const form = new FormData()
form.append('account', accountId)
await fetch(`/v1/api/accounts/orders?symbol=${encodeURIComponent(symbol)}`, {
method: 'DELETE',
headers: { 'TZ-API-KEY-ID': key, 'TZ-API-SECRET-KEY': secret },
body: form,
})
// 2. Read the current position. For options, the position row's `symbol`
// is the underlying ticker - match the user-supplied filter on either
// `symbol` (underlying) or `tradedSymbol` (OCC) to handle both.
const positions = await getPositions()
const target = symbol.toUpperCase()
const pos = positions.find(
(p) => p.symbol.toUpperCase() === target || (p.tradedSymbol?.toUpperCase() ?? '') === target,
)
if (!pos || !pos.shares || pos.shares === 0) return // nothing to close
// 3. Place a closing order in the correct direction.
// Use tradedSymbol (OCC) for options, plain symbol for stocks.
const action = pos.shares > 0 ? 'Sell' : 'Cover'
const { side, openClose } = resolveOrderAction(action)
const extended = isExtendedHours()
const orderSymbol = pos.tradedSymbol ?? pos.symbol
const lastPrice = extended ? (await getQuote(orderSymbol)).price : undefined
await placeOrder({
securityType: pos.securityType as 'Stock' | 'Option',
symbol: orderSymbol,
side,
openClose,
orderQuantity: Math.abs(pos.shares),
orderType: extended ? 'Limit' : 'Market',
timeInForce: extended ? 'Day_Plus' : 'Day',
...(extended && lastPrice ? { limitPrice: lastPrice } : {}),
})
}
You can liquidate a partial size by multiplying Math.abs(pos.shares) by a fraction (e.g. 0.5 to close half the position). Always round to a whole share with Math.max(1, Math.round(...)).
Additional resources
- Open Positions - read positions and P&L, including how filled orders become position rows.
- Short Locates - reserve borrow for hard-to-borrow symbols before placing a Short order.
- Account Information - buying power, margin, and account-level risk metrics.
- Options Trading - single-leg and multi-leg options orders using the same
POST /orderendpoint. - Portfolio WebSocket - real-time order state changes without polling.
- P&L WebSocket - real-time account P&L, balance, and exposure stream.
- API Reference - POST /order - auto-generated OpenAPI reference.
- API Reference - GET /orders - auto-generated OpenAPI reference.
Recipes
Ready-to-run recipes for the patterns described above:
- Place Your First Order - buying-power check, submit, then read back the fill state. The fastest path from API key to a working order.
- Limit Order and Cancel - submit a resting limit, confirm it's working, cancel, and poll until the row reaches a terminal status.
- Watch a Single Order - poll
GET /order/{clientOrderId}until the order reaches a terminal state, with backoff and timeout. - Poll Orders & Detect Fills - track a set of
clientOrderIds acrossGET /ordersand emit transition events when statuses change. - Cancel All Open Orders - bulk cancel via
DELETE /accounts/ordersplus the per-order fallback for stragglers. - Pre-Trade Validation - verify account status, buying power, route, position direction, and option level before submitting.
- Trader Actions Wire Format - round-trip the four trader actions (Buy / Sell / Short / Cover) through
side+openCloseand decode them back fromGET /orders. - Stop-Loss on a Long Position - read
/positions, attach a stop order at a configurable percentage belowpriceAvg, and watch the row close out when the stop is hit. - End-of-Day Recap - pull historical fills and produce a daily trade recap summary.
- Route-Aware Order - query
/routes, pick a destination bysecurityTypes/orderTypes, and submit with an explicitroute. clientOrderIdIdempotency - pre-check, generate, and reconcile so a network retry never produces a duplicate order.