Skip to content
Skip to main content

Equity Trading

Place, monitor, and cancel equity orders against TradeZero's order-management system using seven REST endpoints. 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

MethodPathPurpose
GET/v1/api/accounts/{accountId}/routesList available routing destinations and the order-type / TIF combinations each supports
GET/v1/api/accounts/{accountId}/ordersToday'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}/orderPlace a new order
DELETE/v1/api/accounts/{accountId}/orders/{orderId}Cancel a specific order by clientOrderId
DELETE/v1/api/accounts/ordersCancel 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 Get API Keys for setup. Open positions and P&L are covered on the Open Positions page.


Trader actions

Four trader actions drive equity orders. Each maps to a combination of side and openClose on the request body:

ActionsideopenCloseWhen to use
BuyBuyOpenEnter a new long position
SellSellCloseExit an existing long position
ShortSellOpenEnter a new short position (borrows shares)
CoverBuyCloseExit 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.

Check borrow availability for TSLA
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/is-easy-to-borrow/symbol/TSLA' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Response
{ "isEasyToBorrow": true }
FieldTypeMeaning
isEasyToBorrowbooleantrue - 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

ConditionStatusBody
Successful POST /order (order received, even if orderStatus is Rejected)200JSON
Successful GET200JSON
Successful DELETE /accounts/orders (cancel-all)200{"message":"Cancel Request Submitted Successfully"}
JSON Schema validation error (bad enum, wrong type, out-of-range quantity)400Plain text, bullet format
Account ID mismatch in POST /order path400{"statusCode":"BadRequest","message":"PlaceOrderWithResponse","detail":"Account for User was not found, or User doesn't have entitlements."}
Account ID mismatch in DELETE /orders/{id} path400{"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 endpoints404Not found (plain text)
Account ID mismatch on GET /orders404Not found (plain text)
Wrong HTTP method on /order (GET, PUT, PATCH)405405 method not allowed (plain text)
OPTIONS on /order (CORS preflight)200empty body, Allow: POST header
Wrong HTTP method on /orders (e.g. POST against the plural endpoint)404Not found (plain text)
HEAD on /orders405empty body
URL path with uppercase segments (e.g. /V1/API/...)404Not 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:

  • POST /order returns 200 OK when the request body is valid. Read orderStatus in the response to see whether the order was accepted for routing — rejected orders (off-hours market order, duplicate clientOrderId, insufficient buying power) also return HTTP 200 with orderStatus: "Rejected".
  • Auth failures return different status codes by endpoint. GET and POST endpoints return 404 Not found when auth headers are missing or invalid. The DELETE /orders/{id} cancel endpoint returns 401 Unauthorized with a JSON body instead. Handle both.
  • Account mismatch on POST/DELETE returns 400, not 404. A GET with the wrong account ID returns 404; a POST /order or DELETE /orders/{id} with an account that doesn't match your keys returns a 400 JSON body. The two endpoints share the same {statusCode, message, detail} envelope but use different message values (PlaceOrderWithResponse for POST /order, CancelOrderWithResponse for DELETE /orders/{id}). For TradeZero America accounts, this same 400 appears when the portal login is used instead of the 2TZ account number - use the account value from GET /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

Buy 1 AAPL at market (paper account — route optional)
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/order' \
-X POST \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}' \
-d '{
"securityType": "Stock",
"symbol": "AAPL",
"side": "Buy",
"openClose": "Open",
"orderType": "Market",
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "my-buy-001"
}'
Short 10 SPY at limit $500 GTC (live account — route required)
curl 'https://webapi.tradezero.com/v1/api/accounts/TTE12345678/order' \
-X POST \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_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

FieldTypeRequiredNotes
securityTypestringYes"Stock" for equities and ETFs. Use "Option" or "Mleg" for options (see Options Trading). Case-sensitive.
symbolstringYesTicker 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").
sidestringYes"Buy" or "Sell". Case-sensitive. See Trader actions for how Buy/Sell/Short/Cover map to this field.
openClosestringYes†"Open" to enter a new position; "Close" to exit one. See Trader actions. Always send "Open" or "Close" with that exact casing — "open" or "OPEN" returns HTTP 200 with orderStatus: "Rejected".
orderTypestringYes"Market", "Limit", "Stop", or "StopLimit". Case-sensitive.
orderQuantitynumberYesInteger 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.
timeInForcestringYesOne of the eight values in Time in force. Case-sensitive.
limitPricenumberConditionalRequired 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.
stopPricenumberConditionalRequired when orderType is "Stop" or "StopLimit".
clientOrderIdstringRecommendedYour identifier for the order. If omitted, the server generates a numeric one. Used for cancel-by-id and deduplication. See Order identity.
routestringRecommendedRouting 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.

Order accepted - PendingNew (paper account — route auto-assigned)
{
"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:

Order accepted - PendingNew (live account submitted via SMART)
{
"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.

Order rejected - duplicate clientOrderId
{
"orderStatus": "Rejected",
"text": "R114: Invalid duplicate UserOrderId",
"clientOrderId": "my-buy-001",
"canceledQuantity": 1,
"executed": 0,
"leavesQuantity": 0
}

Response fields

FieldTypeNotes
accountIdstringThe account the order was placed on.
clientOrderIdstringYour 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.
orderStatusstring"PendingNew" - accepted, routing in progress; "New" - acknowledged at venue, resting in book; "Filled" - fully executed; "Canceled" - successfully canceled; "Rejected" - declined (read text for reason).
textstring | nullHuman-readable reason when orderStatus is "Rejected". null when accepted.
executednumberShares filled so far.
leavesQuantitynumberShares still open. 0 once the order is terminal.
canceledQuantitynumberShares canceled.
orderQuantitynumberOriginal quantity requested.
limitPricenumberThe limit price sent. 0 if not a limit order.
priceStopnumberThe stop price sent. 0 if not a stop order.
priceAvgnumberVolume-weighted average fill price. 0 while unfilled.
openClosestring"Open", "Close", or "Unknown" (the server may normalize to "Unknown" in some early-rejection scenarios).
routestringThe routing destination the server used. Echoes the route name ("PAPER", "SMART", …) on accepted orders and on most rejected orders. Some validation-time rejections show the literal "<no value>" when no venue was selected — treat rejection by checking orderStatus === "Rejected" and reading text.
startTimestringISO 8601 timestamp when the order was received.
lastUpdatedstringISO 8601 timestamp of the most recent state change.
tradedSymbolstringFor stocks, equals symbol. For options, the OCC contract string.
strikePricenumber0 for equities.
legCountnumber0 for single-leg orders.
legsarray | nullnull 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.

Fetch today's orders
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/orders' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Response envelope (paper account, one resting limit order)
{
"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 pushNotes
accountIdaccountSame value, different key.
clientOrderIduserOrderId, format "{accountId}:{clientOrderId}"Split userOrderId on the first : to recover the bare id.
canceledQuantitycancelledQuantitySpelling difference.
lastQuantitylastQty
leavesQuantityleavesQuantity and lvsQtyBoth present on WS rows.
maxDisplayQuantitymaxDisplayQty
orderStatusstatusSame 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 noteDetail
tradedSymbolEquals symbol for stocks. For options, symbol is the underlying ticker (e.g. "AAPL") and tradedSymbol carries the full OCC contract (e.g. "AAPL260717C00600000").
textPlain message string, or null. Carries the rejection reason (R##: …) for Rejected rows; usually null on accepted rows. Paper rows may show "TRAFIX_SIM".
legCount / legs0 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.

Fetch a single order
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/order/my-buy-001' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_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": [] }.

Rows here are trade-level, not order-level

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.

Historical orders from 2026-05-01 onward
curl 'https://webapi.tradezero.com/v1/api/accounts/TTE12345678/orders/start-date/2026-05-01' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Response envelope - one row per fill
{
"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": ""
}
]
}
FieldTypeNotes
tradeIdintegerUnique identifier for this individual fill (the primary key on every row).
accountIdstringAccount the trade settled on.
symbolstringSymbol traded.
securityTypestring"Stock" or "Option".
sidestring"Buy" or "Sell" (no enriched "SellShort" / "BuyToCover" here).
qtyintegerShares filled on this execution (not the originating order's quantity).
pricenumberPer-share fill price.
grossProceedsnumberqty × price with sign - negative on Buys (cash out), positive on Sells.
netProceedsnumbergrossProceeds minus totalFees and commission, sign-preserved.
commissionnumberCommission charged on this fill (often 0 on TradeZero accounts).
totalFeesnumberAll non-commission fees (regulatory, exchange, etc.).
currencystringSettlement currency ("USD").
tradeDatestring (date-time)When the trade executed.
settleDatestring (date-time)T+1 settlement date.
entryDatestring (date-time)When the originating order was entered.
execTimestringHH:MM:SS local time of the execution.
canceledbooleantrue if the trade was busted/cancelled post-execution; skip these for activity counts.
mLegIdintegerMulti-leg ticket ID. 0 for single-leg orders.
spreadTypeintegerInternal spread classifier; 0 for ordinary single-leg fills.
notesstringFree-form notes string from the execution venue (often empty).
Paper accounts have no order history

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:

FormatExampleAccepted
YYYY-MM-DD2026-05-01
ISO 8601 with T and Z2026-05-01T00:00:00Z
ISO 8601 with UTC offset (URL-encoded)2026-05-01T00%3A00%3A00-04%3A00
Unpadded month/day2026-5-1
US-style MM-DD-YYYY05-01-2026
Far future2099-12-31✓ (empty array)
Unix epoch start1970-01-01✓ (empty array)
Compact YYYYMMDD20260501✗ - returns 404
Non-date stringnot-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 paramDefaultNotes
numberOfDays30Window length from startDate, up to 365 days
offset0Rows to skip (pagination)
limit100Page size (max 100)
symbolOptional 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.

Cancel by clientOrderId
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/orders/my-buy-001' \
-X DELETE \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}'
Cancel behavior - three distinct failure modes

Cancel-by-id has three different failure shapes:

  1. 401 Unauthorized (JSON body) - auth headers are missing:

    { "statusCode": "Unauthorized", "message": "Token not provided", "detail": null }
  2. 404 Not found (plain text) — the order was not found. This covers: wrong clientOrderId, the order is not yet registered (poll GET /orders after POST /order before canceling), the order is already terminal (Filled, Canceled, Rejected, Expired), or auth headers that do not grant access.

  3. 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.

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.

Cancel all orders on the account
curl 'https://webapi.tradezero.com/v1/api/accounts/orders' \
-X DELETE \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}' \
-F 'account=TZP12345678'
Cancel all AAPL orders only
curl 'https://webapi.tradezero.com/v1/api/accounts/orders?symbol=AAPL' \
-X DELETE \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_SECRET}' \
-F 'account=TZP12345678'
Response (200 OK)
{
"message": "Cancel Request Submitted Successfully"
}
Cancel-all in TypeScript / fetch
const form = new FormData();
form.append('account', accountId);
const res = await fetch(
'https://webapi.tradezero.com/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.

List routes
curl 'https://webapi.tradezero.com/v1/api/accounts/TZP12345678/routes' \
-H 'TZ-API-KEY-ID: {YOUR_CLIENT_ID}' \
-H 'TZ-API-SECRET-KEY: {YOUR_CLIENT_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:

Response (paper account - 2 routes)
{
"routes": [
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "RangeOrder"],
"routeName": "PAPER",
"securityTypes": ["Stock", "Option"],
"timesInForce": ["Day", "GoodTillCancel", "GoodTillCrossing"],
"useDisplayQty": false
},
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "RangeOrder"],
"routeName": "PAPERM",
"securityTypes": ["MLEG"],
"timesInForce": ["Day"],
"useDisplayQty": false
}
]
}
Response (live account - 5 routes, typical)
{
"routes": [
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "MarketOnClose", "LimitOnClose", "RangeOrder", "TrailStop"],
"routeName": "SMART",
"securityTypes": ["Stock"],
"timesInForce": ["Day", "GoodTillCancel", "AtTheOpening", "Day_Plus", "GTC_Plus"],
"useDisplayQty": true
},
{
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "TrailStop"],
"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 typically exposes:

RouteSecurity typesNotes
SMARTStockTradeZero's smart order router for equities. Supports the widest set of equity order types and TIFs (Day_Plus, GTC_Plus, AtTheOpening, MarketOnClose, LimitOnClose, TrailStop, RangeOrder) and advertises useDisplayQty: true for iceberg orders.
CTDLStockDirect-access route to a single venue. Useful when you want deterministic routing rather than smart-router discretion. Supports TrailStop.
SMARTOOptionSmart router for single-leg options.
SMARTMOption, MLEGSmart router for multi-leg option spreads. Day-only.
ARCAStockDirect 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.

Order types accepted by POST /order

POST /order accepts "Limit", "Market", "Stop", and "StopLimit". 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

orderTypeRequired price fieldsNotes
MarketNoneExecutes at the best available price. Rejected outside regular trading hours (9:30 AM-4:00 PM ET) on both paper and live accounts.
LimitlimitPriceExecutes at limitPrice or better. Rests in the book if the price is not immediately available.
StopstopPriceBecomes a market order when stopPrice is touched.
StopLimitstopPrice + limitPriceBecomes 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.

timeInForceCommon nameWhen the order is active
DayDayRegular session only (9:30 AM-4:00 PM ET). Expires at close if unfilled.
GoodTillCancelGTCPersists across sessions until manually canceled or filled. Valid for Limit, Stop, and StopLimit on SMART routing.
Day_PlusDay+Extended-hours session (4:00 AM-8:00 PM ET). Limit orders only. The extended-hours coercion used by some clients (Market → Limit, Day → Day_Plus) is appropriate for pre/post-market trading.
GTC_PlusGTC+Like GTC but active during extended hours. Limit orders only.
AtTheOpeningOPG / MOO / LOOSubmits 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.
ImmediateOrCancelIOCFill immediately or cancel the unfilled portion. Limit orders during regular hours. Not available on paper accounts.
FillOrKillFOKFill the entire quantity immediately or cancel the whole order. Direct routing only; not available on SMART or paper accounts.
GoodTillCrossingGTXDeprecated on SMART routing. If used on a non-SMART direct route, means "valid for the current trading session." Avoid on SMART.
Paper vs. live TIF restrictions

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.


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.

Condition400 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 literalinvalid 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-quoteinvalid 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 parseableinvalid 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 bodyinvalid character '<' looking for beginning of value
Form-urlencoded or plain-text body404 Not found

For side, orderType, securityType, and timeInForce, wrong case (e.g. "buy", "LIMIT") returns 400. For openClose, always send "Open" or "Close" exactly — wrong case returns HTTP 200 with orderStatus: "Rejected".

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.

text field valueCondition
"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
nullOrder rejected for a reason not surfaced as an R-code (e.g. some buying-power or risk checks). Inspect orderStatus, the route, and account state to diagnose.

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).

Extra unknown fields

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.

Stop-Limit price coherence

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.

Live exchange limits the client ID to 36 characters

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 fires for 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 cancellation

A 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 classBehavior
Alphanumerics, hyphens, underscoresSafe - recommended
Colons (:)Accepted by the server
Forward slashes (/)Accepted by the server
SpacesAccepted 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

In practice, 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-generate

Sending 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".

R95 rejection response
{
"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 typical limit order lifecycle looks like this:

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):

orderStatusTerminal?Meaning
PendingNewNoGateway has accepted the order; routing in progress. Usually transitions to New within milliseconds.
AcceptedNoOrder acknowledged by the venue (most commonly seen on Portfolio WebSocket pushes). Functionally equivalent to New for blotter purposes - treat both as working.
NewNoOrder is resting in the order book at the venue, waiting to be filled.
PartiallyFilledNoSome shares have filled; order is still working for the remainder.
FilledYesFully executed. executed equals orderQuantity, leavesQuantity is 0.
PendingCancelNoCancel request received; awaiting confirmation from venue.
CanceledYesSuccessfully canceled. canceledQuantity is set to the canceled-out shares.
RejectedYesDeclined. Read text for the R-code reason. canceledQuantity is typically 1.
DoneForDaySemiOrder expired at end of session. For GoodTillCancel and GTC_Plus orders it will resurface the next session; for all others it is effectively terminal.
ExpiredYesOrder 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, a 1–2 second delay before polling /orders gives the new row time to appear. For real-time order status without polling, subscribe to the Portfolio WebSocket stream, which pushes order state changes (PendingNew, Accepted, Filled, Canceled, etc.) as they occur.

Terminal statuses: Filled, Canceled, Rejected, Expired. An order in a terminal state cannot be canceled - attempting to do so returns 404 Not found. 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:

  1. DELETE /v1/api/accounts/{accountId}/orders/{originalClientOrderId} - cancel the working order.
  2. Wait for the cancellation to settle. Poll GET /orders until the original order's orderStatus is Canceled.
  3. POST /v1/api/accounts/{accountId}/order - submit a fresh order with a new clientOrderId and 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();
}
Why a fresh 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.

Race window between cancel and replacement

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

Enter a long position with a limit order, then attach a 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.tradezero.com/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).');
default:
// Cancel-by-id rejections come back from DELETE /orders/{id}, not POST /order.
// null text = price/buying-power rejection; log and handle.
console.warn('Order rejected:', data.text ?? '(no reason)');
}
}

Error handling

Every POST /order call requires two levels of error checking:

  1. HTTP-level - !res.ok catches 400 (schema error, plain-text body), 401 (cancel endpoint auth), 404 (auth/account on GET and POST), and 405 (wrong method).
  2. Application-level - the HTTP status can be 200 while the order is still rejected. Always read orderStatus after parsing.
Error handling template
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 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, Rejected, Expired), or auth headers that do not grant access. 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.

ShapeEndpointsAccount-id fieldClient-id fieldQuantity fields
CleanPOST /order; GET /order/{clientOrderId}; GET /orders (each row); Mleg GET /order/{cid} before legs[] is populatedaccountIdclientOrderId (bare)canceledQuantity, lastQuantity, leavesQuantity, maxDisplayQuantity
WebSocket (enriched)Portfolio WebSocket Order push; GET /order/{clientOrderId} for Mleg orders once legs[] is populatedaccountuserOrderId = "{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 (accountaccountId, split userOrderId into accountId + clientOrderId, cancelledQuantitycanceledQuantity, lastQtylastQuantity, maxDisplayQtymaxDisplayQuantity). Run it on every Portfolio WebSocket Order message and every GET /order/{cid} response that has a non-null legs[]. REST responses from POST /order, GET /orders, and most GET /order/{cid} calls are already in the clean shape.
  • Pick clientOrderId as your stable join key. After normalization it's identical across all four sources, and the same value you sent on POST /order.
  • Seed your local cache from GET /orders on startup (the today's-orders list including any working multi-day GTCs), then apply Portfolio WebSocket pushes to keep it live. Treat orderStatus: "Filled", "Canceled", "Rejected", "Expired", or "DoneForDay" as terminal.
  • The side field on GET /orders rows is enriched for short/cover trades - see The side field - enriched values below. WebSocket pushes carry the raw "Buy" / "Sell" you sent. If you display labels from WS messages, derive them yourself from side + openClose.
  • text carries the rejection reason for Rejected rows ("R##: …") and is usually null for accepted rows; the paper simulator may put "TRAFIX_SIM" on paper rows. Check text explicitly 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 requestopenClose in requestside in GET /orders responseTrader-facing label
"Buy""Open""Buy"Buy
"Sell""Close""Sell"Sell
"Sell""Open""SellShort" ✓ confirmedShort
"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 ordersNew, 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 Track Orders Across Sessions recipe for an end-to-end implementation.

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:

  • MarketLimit at the last traded price (or mid-point of bid/ask)
  • DayDay_Plus
  • GTC / 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 often size orders dynamically rather than accepting a fixed share count. Common sizing strategies:

StrategyCalculation
Fixed sharesquantity = config.shares
Dollar amountquantity = Math.floor(dollarAmount / lastPrice)
% of buying powerquantity = Math.floor((buyingPower * pct) / lastPrice)
% of current positionquantity = 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, most clients derive it from the current quote automatically. The pattern is to pick a price source and apply an optional offset (dollar or percentage). A typical Buy uses ask + small offset; a Sell uses bid - small offset; stop orders commonly derive stopPrice from lowOfDay (longs) or highOfDay (shorts).

Common price sources:

SourceDescription
bidNational best bid
askNational best ask
lastLast traded price
midpoint(bid + ask) / 2
highOfDayToday's session high
lowOfDayToday's session low
avgEntryThe 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.
// limitPrice is required for Limit orders - omitting it causes the
// 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

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 across GET /orders and emit transition events when statuses change.
  • Cancel All Open Orders - bulk cancel via DELETE /accounts/orders plus 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 + openClose and decode them back from GET /orders.
  • Stop-Loss on a Long Position - read /positions, attach a stop order at a configurable percentage below priceAvg, and watch the row close out when the stop is hit.
  • Track Orders Across Sessions - pair GET /orders (today) with GET /orders/start-date/{date} (history) to reconstruct a full session-level picture.
  • End-of-Day Recap - pull historical fills and produce a daily trade recap summary.
  • Route-Aware Order - query /routes, pick a destination by securityTypes / orderTypes, and submit with an explicit route.
  • clientOrderId Idempotency - pre-check, generate, and reconcile so a network retry never produces a duplicate order.