Options Trading
Options trading uses the same POST /v1/api/accounts/{accountId}/order endpoint as equity orders. The only difference is securityType: set it to "Option" for single-leg orders or "Mleg" for multi-leg spreads. There is no dedicated option chain, expiry lookup, or greeks endpoint in the TradeZero REST API - market data for option chains is sourced separately.
At a glance
| Method | Path | Purpose |
|---|---|---|
GET | /v1/api/accounts/{accountId}/routes | List routing destinations - filter by securityType containing "Option" or "MLEG" |
GET | /v1/api/accounts/{accountId}/orders | Today's orders, including option and spread rows |
GET | /v1/api/accounts/{accountId}/orders/start-date/{startDate} | Historical orders from a given date (one row per fill; up to 1 week of history) |
POST | /v1/api/accounts/{accountId}/order | Place a single-leg (securityType: "Option") or multi-leg (securityType: "Mleg") order |
DELETE | /v1/api/accounts/{accountId}/orders/{orderId} | Cancel a specific order by clientOrderId |
DELETE | /v1/api/accounts/orders | Cancel all open orders (optionally scoped to a symbol) |
GET | /v1/api/accounts/{accountId}/positions | Open positions - option rows carry putCall, priceStrike, tradedSymbol |
Authentication and all HTTP semantics are identical to Equity Trading. Refer to that page for auth header details, error handling, clientOrderId rules, and the full cancel workflow.
OCC symbol format
All option order symbols follow the compact OCC format - no spaces, no separators:
{UNDERLYING}{YYMMDD}{C|P}{strike × 1000, zero-padded to 8 digits}
| Component | Length | Example |
|---|---|---|
| Underlying ticker | 1-6 chars | AAPL |
Expiration: YYMMDD | 6 digits | 260717 = Jul 17 2026 |
| Put/Call flag | 1 char | C = Call, P = Put |
| Strike × 1000, padded to 8 digits | 8 digits | 00600000 = $600.00 |
Full example: AAPL260717C00600000 - AAPL call, Jul 17 2026, $600 strike.
function buildOccSymbol(
underlying: string,
expiration: Date,
callPut: 'Call' | 'Put',
strike: number,
): string {
const yy = expiration.getFullYear().toString().slice(-2)
const mm = (expiration.getMonth() + 1).toString().padStart(2, '0')
const dd = expiration.getDate().toString().padStart(2, '0')
const cp = callPut === 'Call' ? 'C' : 'P'
const strikeStr = Math.round(strike * 1000).toString().padStart(8, '0')
return `${underlying.toUpperCase()}${yy}${mm}${dd}${cp}${strikeStr}`
}
Schema validation: The server validates the symbol string against the pattern ^[a-zA-Z0-9._-]+$ - it does not structurally validate that the string is a valid OCC symbol. Strings with spaces, slashes, @, an empty string, or the OSI long-form (a 21-character symbol right-padded with spaces) all fail the regex and return 400 with - symbol: Does not match pattern '^[a-zA-Z0-9._-]+$'.
Pattern-valid strings that don't correspond to a listed contract - non-existent strikes (AAPL260619C99999000), past-expiry contracts (AAPL250117C00200000), unrecognized roots - pass schema validation and produce a normal PendingNew response. Whether the order then prices, rests, or is rejected depends on whether the contract resolves to a live instrument at the chosen route. Always source symbol, expiration, and strike from a current option chain feed before sending an order.
No spaces: Always send the compact OCC string. The OSI long-form (right-padded with spaces to 21 characters) is not accepted.
Single-leg options (securityType: "Option")
Trader actions
Four actions map to side + openClose combinations:
| Action | side | openClose | Description |
|---|---|---|---|
| Buy to Open (BTO) | "Buy" | "Open" | Purchase a new option contract - long calls, long puts, and the long legs of any debit spread |
| Sell to Open (STO) | "Sell" | "Open" | Write a new option contract - covered calls (against held stock), cash-secured puts, and the short legs of any spread |
| Sell to Close (STC) | "Sell" | "Close" | Sell an existing long option to exit |
| Buy to Close (BTC) | "Buy" | "Close" | Buy back a short option to exit |
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
securityType | string | Yes | "Option". Case-sensitive - "option" returns 400 with - securityType: securityType must be one of the following: "Stock", "Option", "Mleg". |
symbol | string | Yes | The full OCC option symbol, e.g. "AAPL260717C00600000". Same character pattern as equities (^[a-zA-Z0-9._-]+$). |
side | string | Yes | "Buy" or "Sell". Case-sensitive. |
openClose | string | Recommended | "Open" or "Close". Always send properly cased "Open" or "Close" so order history records the correct trader action (BTO / STO / BTC / STC). |
orderType | string | Yes | "Market", "Limit", "Stop", or "StopLimit". Case-sensitive. |
orderQuantity | integer | Yes | Number of contracts. Minimum 1, maximum 1,000,000 (> 1e6 returns 400 - orderQuantity: Must be less than or equal to 1e+06). Fractional values return 400 - orderQuantity: Invalid type. Expected: integer, given: number. |
timeInForce | string | Yes | One of the eight schema-valid values in the TIF table. |
limitPrice | number | Conditional | Required for "Limit" and "StopLimit". Expressed as dollars per share of premium (the standard option quoting convention) - 3.50 means $3.50 per share, which translates to $350 of total premium per contract since US equity option contracts represent 100 shares. Values above 9999.99 return 400 - limitPrice: Must be less than or equal to 9999.99. Negative values return orderStatus: "Rejected" with text: "R145: Negative Price Is Not Allowed". |
stopPrice | number | Conditional | Required for "Stop" and "StopLimit". Always send a positive value — Stop and StopLimit orders without a valid stop price return orderStatus: "Rejected" with text containing R88 or R145. |
clientOrderId | string | Recommended | Your identifier for deduplication and cancel-by-id. Keep ≤ 36 characters for live-exchange compatibility. |
route | string | Recommended | Routing destination. Query /routes first and send an explicit routeName whose securityTypes includes "Option" (e.g. SMARTO on a live account). On live accounts, always send route — omitting it can leave the order with no route assigned and produce R54: Unable to reach the destination Route. Paper accounts auto-assign PAPER when route is omitted. Route names vary by account — query /routes and use the value returned for your account. |
Request example (single-leg)
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": "Option",
"symbol": "AAPL260717C00600000",
"side": "Buy",
"openClose": "Open",
"orderType": "Limit",
"limitPrice": 3.50,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "bto-aapl-call-001"
}'
Response (single-leg)
{
"accountId": "TZP12345678",
"canceledQuantity": 0,
"clientOrderId": "bto-aapl-call-001",
"executed": 0,
"lastPrice": 0,
"lastQuantity": 0,
"lastUpdated": "2026-05-13T19:22:53.667Z",
"leavesQuantity": 1,
"legCount": 0,
"legs": null,
"limitPrice": 3.50,
"maintenanceRequirement": 0,
"marginRequirement": 0,
"maxDisplayQuantity": 0,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "PendingNew",
"orderType": "Limit",
"priceAvg": 0,
"priceStop": 0,
"route": "PAPER",
"securityType": "Option",
"side": "Buy",
"startTime": "2026-05-13T19:22:53.667Z",
"strikePrice": 600,
"symbol": "AAPL",
"text": null,
"timeInForce": "Day",
"tradedSymbol": "AAPL260717C00600000"
}
This is the clean response shape used by POST /order, GET /order/{clientOrderId}, and GET /orders. Portfolio WebSocket Order pushes use a WebSocket field set — normalize userOrderId, cancelledQuantity, and related aliases when merging stream data with REST.
The same submission against a live account echoes back the live account id and "route": "SMARTO" (the live single-leg-options smart router); the rest of the clean response shape is identical.
Response fields (option-specific)
| Field | Notes |
|---|---|
symbol | The underlying ticker (e.g. "AAPL"), not the full OCC symbol. |
tradedSymbol | The full OCC symbol (e.g. "AAPL260717C00600000") for accepted orders. May be null on certain early-rejection rows. |
strikePrice | The strike price as a number (e.g. 600). Populated for accepted orders; may be 0 on rejected rows. |
securityType | Echoed as "Option" for single-leg orders. |
openClose | Echoes the value sent. The server may surface "Unknown" on certain early-rejection paths where it could not parse the trader action. |
legCount | 0 for single-leg orders. |
legs | null for single-leg orders. |
Time in force for options
The timeInForce field is filtered at two layers: the JSON-Schema enum (which determines whether the request is well-formed) and the route's timesInForce list on /routes (which determines whether the route will honor the value).
Schema - exactly eight values pass validation. Anything else returns 400 - timeInForce: timeInForce must be one of the following: ...:
Day, GoodTillCancel, AtTheOpening, ImmediateOrCancel,
FillOrKill, GoodTillCrossing, Day_Plus, GTC_Plus
MarketOnClose and LimitOnClose are not part of the option-order schema and return 400 even on routes that conceptually support them.
Route - what each route advertises in its timesInForce list, and what it supports at execution time:
timeInForce | PAPER (single-leg paper) | PAPERM (Mleg paper) | SMARTO (single-leg live) | SMARTM (Mleg live) |
|---|---|---|---|---|
Day | Advertised + accepted | Advertised + accepted | Advertised + accepted | Advertised + accepted |
GoodTillCancel | Advertised + accepted | Accepted on paper | Advertised + accepted | Accepted on live |
GoodTillCrossing | Advertised + accepted | Accepted on paper | Accepted on live | Accepted on live |
Day_Plus | Accepted on paper | Accepted on paper | Use a route that advertises it | Use a route that advertises it |
GTC_Plus | Accepted on paper | Accepted on paper | Use a route that advertises it | Use a route that advertises it |
AtTheOpening | Accepted on paper | Accepted on paper | Use a route that advertises it | Use a route that advertises it |
ImmediateOrCancel | Accepted on paper | Accepted on paper | Use a route that advertises it | Use a route that advertises it |
FillOrKill | Accepted on paper | Accepted on paper | Use a route that advertises it | Use a route that advertises it |
The paper environment accepts every schema-valid TIF — orders move to PendingNew regardless of whether the TIF is advertised on the route. This applies to paper accounts only. On live, the TIFs honored by a given route are exactly those listed in timesInForce on that route's /routes entry — pick from that list when sending live orders.
/routesThe route's timesInForce list defines which TIF values a live route honors. Query GET /routes, pick a route whose securityTypes includes "Option" or "MLEG", and choose a timeInForce from that route's timesInForce list. Paper accounts accept any schema-valid TIF even when the route does not advertise it.
Multi-leg options (securityType: "Mleg")
Envelope rules
Multi-leg orders use a two-part structure - an outer envelope describing the order as a whole, and a legs array where each leg carries its own direction.
Critical rules:
securityTypemust be"Mleg"- not"mleg"or"MLEG"(returns400if wrong case).symbolmust be the underlying ticker (e.g."AAPL"), not an OCC symbol. The envelopesymboland every leg's underlying must match - sending different underlyings across legs is accepted at schema and asynchronously rejected at HTTP200withtext: "R152: Validation Order Error: Malformed Multi Leg Order - Leg And Root Symbol Mismatch".- No
sideat the root level. Mleg envelopes express direction per leg only — omit rootside. If present, the API returns400with a schema validation error. - Each leg in
legs[]carries its ownside,ratio, andopenClose- all three are schema-required (unlike single-leg, whereopenCloseis not enforced). - Minimum 2 legs; maximum 4 legs.
1leg returns"Too few legs, invalid order.";5+legs return"Too many legs, invalid order.". - Leg
ratiomust be an integer between1and100inclusive. The stock leg of covered call / married put must be exactly100. - All legs must share the same expiration. Mixed expirations return
"Calendar orders are not currently supported.". - Leg ratios must not share a common divisor
> 1.2:2returns"Invalid order, leg ratios cannot have a common devisor."; reduce to1:1.
Request fields - envelope
| Field | Type | Required | Notes |
|---|---|---|---|
securityType | string | Yes | "Mleg". Case-sensitive. |
symbol | string | Yes | The underlying ticker, e.g. "AAPL" or "SPY". Always uppercase. |
side | - | Omit | Must not appear at the root level. Each leg carries its own side. |
orderType | string | Yes | "Market", "Limit", "Stop", or "StopLimit". |
orderQuantity | integer | Yes | Number of spread units (contracts). |
limitPrice | number | Conditional | The net price for the entire spread as a positive number. For debit spreads this is what you pay; for credit spreads this is what you collect. Always pass an absolute (positive) value. Required for "Limit" and "StopLimit". |
stopPrice | number | Conditional | Required for "Stop" and "StopLimit". If omitted for "StopLimit", the limitPrice value is used as a fallback. |
timeInForce | string | Yes | Day is the only TIF advertised by both the paper PAPERM and live SMARTM Mleg routes. |
clientOrderId | string | Optional | Used for cancel-by-id and deduplication. Mleg orders accept the request without a clientOrderId - the server auto-generates one (e.g. "0514030201713.2183") and echoes it back in the response. Sending your own client-id is still recommended whenever you need to correlate the response, cancel by id, or deduplicate retries. |
route | string | Recommended | Routing destination. On live accounts, send an explicit "SMARTM" (or whatever your /routes response advertises for "MLEG"); the live SMART route is stock-only. On live accounts, always send route — omitting it can leave the order with no route assigned and produce R54: Unable to reach the destination Route. On paper, omitting route is fine: the API assigns "PAPER" for stock-included Mleg envelopes and "PAPERM" for pure-option Mleg envelopes, echoing back whichever destination was selected. Paper also accepts and echoes an explicit value ("PAPER", "PAPERM", "SMARTM", etc.). Always defer to /routes. |
legs | array | Yes | Between 2 and 4 leg objects. |
limitPrice is always positive - it represents the absolute net price for the spread. For a debit spread paying $2.50, send "limitPrice": 2.50. For a credit spread collecting $3.75, also send "limitPrice": 3.75. The direction (debit vs. credit) is implied by which legs are "Buy" and which are "Sell", not by the sign of limitPrice.
Request fields - each leg
| Field | Type | Required | Notes |
|---|---|---|---|
symbol | string | Yes | OCC option symbol for this leg (e.g. "AAPL260717C00600000"). For covered call or married put hybrid strategies, the stock leg uses the bare ticker (e.g. "AAPL"). |
side | string | Yes | "Buy" or "Sell" for this specific leg. |
ratio | integer | Yes | Contract ratio. 1 for most strategies; 2 for the body legs of a butterfly. Range: 1-100. |
openClose | string | Yes | "Open" or "Close". Schema-validated per leg - wrong value returns HTTP 400. |
The server accepts legs in any order, but passing them sorted ascending by strike (lowest strike first) is the recommended convention and matches how the response returns them. For strategies spanning both puts and calls at different strikes, sort all legs ascending by strike across the full array.
Request example (Mleg)
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": "Mleg",
"symbol": "AAPL",
"orderType": "Limit",
"limitPrice": 2.50,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "bull-call-spread-001",
"legs": [
{ "symbol": "AAPL260717C00600000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "AAPL260717C00700000", "side": "Sell", "ratio": 1, "openClose": "Open" }
]
}'
Response (Mleg)
A Mleg order returns two distinct response shapes depending on which endpoint reads it and how far spread processing has progressed:
- Clean shape — returned by
POST /order,GET /orders(each row), andGET /order/{clientOrderId}while the order isPendingNew, terminal-Rejected, or otherwise beforelegs[]is populated. Field names match the single-leg / stock order shape:accountId,clientOrderId,canceledQuantity,lastQuantity,maxDisplayQuantity. - WebSocket shape — returned by
GET /order/{clientOrderId}oncelegs[]is populated (i.e. forFilled,PartiallyFilled, or any state that has crossed into the spread book), and by Portfolio WebSocketOrderpushes. Field names useaccount,userOrderId,cancelledQuantity(double-l),lastQty,maxDisplayQty, plusmlegID,accountType,status,sourceSystem, etc.
The immediate POST /order response always carries legCount: 0 and legs: null — leg details appear once the spread is processed (typically within a few hundred milliseconds). To see the leg breakdown, re-fetch via GET /order/{clientOrderId} once the order has transitioned out of PendingNew. Filled market Mleg orders may drop from GET /orders quickly; the single-order lookup is the reliable path.
Top-level Mleg response fields:
| Field | Notes |
|---|---|
securityType | Echoed as "MLEG" (uppercase), even though the request uses "Mleg" |
symbol | The underlying ticker (e.g. "SPY") |
tradedSymbol | Also the underlying ticker for Mleg orders |
side | Reflects the API-computed net direction (e.g. "Buy" for a debit spread, "Sell" for a credit spread, or "Unknown" / "" while pending). It is not sent on the request. |
openClose | "Open" or "Close" once the spread envelope resolves; "Unknown" for orders rejected before spread processing |
legCount | Number of legs. 0 on the immediate POST response and on rejected envelopes; populated to 2/3/4 once the spread is processed |
legs | null until the spread is processed; then an array of leg execution details |
limitPrice | For Market Mleg orders, the API fills in the limit computed from the legs (e.g. 753.97 for a Buy 100 SPY + Buy 1 SPY put envelope) |
text | "TRAFIX_SIM" on paper-filled rows; null on rejections |
mlegID | (WebSocket shape only) Numeric spread ID. Both the open and the close envelope of the same round-trip share the same mlegID |
Each entry in the legs[] array:
| Field | Description |
|---|---|
index | Zero-based leg position in the envelope |
symbol | OCC option symbol for option legs; underlying ticker for the stock leg of covered call / married put |
side | "Buy" or "Sell" |
qty / ratio | Per-leg contract count (or share count for the stock leg). Wire field is qty; normalize to ratio |
lvsQty | Leaves quantity - remaining unfilled contracts |
lastQty | Last fill quantity |
lastPx | Last fill price |
avgPx | Average fill price for this leg. Normalize to priceAvg |
cxlQty | Canceled quantity |
openClose | "Open" on opening legs; "Close" on closing legs |
The example below shows the same Mleg order at both stages - first the immediate POST /order response (clean shape, legs[] not yet populated), then the same clientOrderId read back from GET /order/{cid} after legs[] is populated (WebSocket shape):
{
"accountId": "TZP12345678",
"canceledQuantity": 0,
"clientOrderId": "married-put-001-open",
"executed": 0,
"lastPrice": 0,
"lastQuantity": 0,
"lastUpdated": "2026-05-14T16:35:57.9956066Z",
"leavesQuantity": 1,
"legCount": 0,
"legs": null,
"limitPrice": 74825.07,
"maintenanceRequirement": 0,
"marginRequirement": 0,
"maxDisplayQuantity": 0,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "PendingNew",
"orderType": "Market",
"pegDifference": 0,
"pegOffsetType": "Price",
"priceAvg": 0,
"priceStop": 0,
"route": "PAPERM",
"securityType": "MLEG",
"side": "Buy",
"startTime": "2026-05-14T16:35:57.9956066Z",
"strikePrice": 0,
"symbol": "SPY",
"text": null,
"timeInForce": "Day",
"tradedSymbol": "SPY"
}
{
"account": "TZP12345678",
"cancelledQuantity": 0,
"executed": 1,
"lastPrice": 753.98,
"lastQty": 1,
"lastUpdated": "2026-05-14T16:35:58.3577181Z",
"leavesQuantity": 0,
"legCount": 2,
"legs": [
{ "index": 0, "symbol": "SPY", "side": "Buy", "qty": 100, "lvsQty": 0, "lastQty": 100, "lastPx": 747.76, "avgPx": 747.76, "cxlQty": 0, "openClose": "Open" },
{ "index": 1, "symbol": "SPY260522P00748000", "side": "Buy", "qty": 1, "lvsQty": 0, "lastQty": 1, "lastPx": 6.22, "avgPx": 6.22, "cxlQty": 0, "openClose": "Open" }
],
"limitPrice": 753.97,
"lvsQty": 0,
"maxDisplayQty": 0,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "Filled",
"orderType": "Market",
"priceAvg": 753.98,
"route": "PAPERM",
"securityType": "MLEG",
"side": "Buy",
"startTime": "2026-05-14T16:35:57.9956066Z",
"status": "Filled",
"symbol": "SPY",
"text": "TRAFIX_SIM",
"timeInForce": "Day",
"tradedSymbol": "SPY",
"userOrderId": "TZP12345678:married-put-001-open"
}
The "shape switch" is driven by legs[] being populated, not by the endpoint or the order status. The same clientOrderId returns the clean shape until legs[] is populated, then returns the WebSocket shape from that point onward - including via GET /order/{cid}. Clients that read fills via GET /order/{cid} should normalize both shapes (see the Working with the option order response section).
Closing a multi-leg position
There is no separate "close spread" endpoint. To unwind a filled spread, send a new Mleg order with the same legs but with each side flipped and each openClose set to "Close". The example below shows the close envelope for the married put above and the resulting filled GET /order/{clientOrderId} response:
{
"securityType": "Mleg",
"symbol": "SPY",
"orderQuantity": 1,
"orderType": "Market",
"timeInForce": "Day",
"route": "PAPERM",
"clientOrderId": "married-put-001-close",
"legs": [
{ "symbol": "SPY", "side": "Sell", "ratio": 100, "openClose": "Close" },
{ "symbol": "SPY260522P00748000", "side": "Sell", "ratio": 1, "openClose": "Close" }
]
}
{
"account": "TZP12345678",
"userOrderId": "TZP12345678:married-put-001-close",
"orderStatus": "Filled",
"executed": 1,
"legCount": 2,
"legs": [
{ "index": 0, "symbol": "SPY", "side": "Sell", "qty": 100, "lvsQty": 0, "lastQty": 100, "lastPx": 747.74, "avgPx": 747.74, "cxlQty": 0, "openClose": "Close" },
{ "index": 1, "symbol": "SPY260522P00748000", "side": "Sell", "qty": 1, "lvsQty": 0, "lastQty": 1, "lastPx": 6.18, "avgPx": 6.18, "cxlQty": 0, "openClose": "Close" }
],
"mlegID": 108908,
"openClose": "Close",
"priceAvg": 753.92,
"limitPrice": 753.99,
"route": "PAPERM",
"securityType": "MLEG",
"side": "Sell",
"symbol": "SPY",
"tradedSymbol": "SPY",
"text": "TRAFIX_SIM",
"timeInForce": "Day"
}
The mlegID (108908 here) is assigned when the open envelope clears the spread book; the matching close envelope inherits the same mlegID. Group filled Mleg lookups by mlegID (via GET /order/{clientOrderId} or rows still visible in GET /orders) to reconstruct each open / close round-trip — see Grouping Mleg order history by spread.
Pure-option Mleg round-trip
The same shape switch happens for pure-option spreads. The example below shows a SPY bull-call vertical (Buy 749C / Sell 750C, both Open, orderType: "Market") at the open and close legs once each has filled:
{
"account": "TZP12345678",
"cancelledQuantity": 0,
"executed": 1,
"lastPrice": 0.55,
"lastQty": 1,
"legCount": 2,
"legs": [
{ "index": 0, "symbol": "SPY260522C00749000", "side": "Buy", "qty": 1, "lvsQty": 0, "lastQty": 1, "lastPx": 6.13, "avgPx": 6.13, "cxlQty": 0, "openClose": "Open" },
{ "index": 1, "symbol": "SPY260522C00750000", "side": "Sell", "qty": 1, "lvsQty": 0, "lastQty": 1, "lastPx": 5.58, "avgPx": 5.58, "cxlQty": 0, "openClose": "Open" }
],
"limitPrice": 0.56,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "Filled",
"orderType": "Market",
"priceAvg": 0.55,
"route": "PAPER",
"securityType": "MLEG",
"side": "Buy",
"status": "Filled",
"symbol": "SPY",
"text": "TRAFIX_SIM",
"timeInForce": "Day",
"tradedSymbol": "SPY",
"userOrderId": "TZP12345678:bull-call-vert-001-open"
}
{
"account": "TZP12345678",
"cancelledQuantity": 0,
"executed": 1,
"lastPrice": 0.51,
"lastQty": 1,
"legCount": 2,
"legs": [
{ "index": 0, "symbol": "SPY260522C00749000", "side": "Sell", "qty": 1, "lvsQty": 0, "lastQty": 1, "lastPx": 6.11, "avgPx": 6.11, "cxlQty": 0, "openClose": "Close" },
{ "index": 1, "symbol": "SPY260522C00750000", "side": "Buy", "qty": 1, "lvsQty": 0, "lastQty": 1, "lastPx": 5.60, "avgPx": 5.60, "cxlQty": 0, "openClose": "Close" }
],
"limitPrice": 0.55,
"openClose": "Close",
"orderQuantity": 1,
"orderStatus": "Filled",
"orderType": "Market",
"priceAvg": 0.51,
"route": "PAPER",
"securityType": "MLEG",
"side": "Sell",
"status": "Filled",
"symbol": "SPY",
"text": "TRAFIX_SIM",
"timeInForce": "Day",
"tradedSymbol": "SPY",
"userOrderId": "TZP12345678:bull-call-vert-001-close"
}
Per-leg avgPx/lastPx show each leg's own venue price (6.13 for the long 749C, 5.58 for the short 750C) while priceAvg at the envelope level is the net debit the spread cleared at (0.55). The close envelope reverses every side and sets openClose: "Close" on each leg.
orderType for Mleg ordersMarketis the simplest choice for 2- and 3-leg pure-option spreads (verticals, straddles, strangles, butterflies, iron butterflies) and for stock-included envelopes (covered call, married put). The API computes the net price from the legs and fills.Limitwith alimitPriceset to your net price target (always a positive absolute value - see tip above) gives you explicit control. For 4-leg condors with four distinct strikes,Limitwith a generous net price (e.g.1.00for a long call condor debit,0.10for a short iron condor net) is the recommended way to express the spread's reservation price.
Supported strategies
The server validates multi-leg orders against supported strategy templates. The combinations below are accepted; other shapes return 400 with a clear JSON detail message.
Supported
The strategy-template validator accepts the shapes below (HTTP 200, orderStatus: "PendingNew"):
| Strategy | Legs | Direction |
|---|---|---|
| Bull call spread (vertical, debit) | 2 | Buy lower-strike call, sell higher-strike call |
| Bear put spread (vertical, debit) | 2 | Buy higher-strike put, sell lower-strike put |
| Bull put spread (vertical, credit) | 2 | Sell higher-strike put, buy lower-strike put |
| Bear call spread (vertical, credit) | 2 | Sell lower-strike call, buy higher-strike call |
| Long straddle | 2 | Buy call + buy put at the same strike |
| Short straddle | 2 | Sell call + sell put at the same strike |
| Long strangle | 2 | Buy OTM call + buy OTM put at different strikes |
| Short strangle | 2 | Sell OTM call + sell OTM put at different strikes |
| Long call butterfly | 3 | Buy 1 low, sell 2 mid, buy 1 high (ratio 1-2-1, calls) |
| Long put butterfly | 3 | Buy 1 high, sell 2 mid, buy 1 low (ratio 1-2-1, puts) |
| Short iron butterfly | 4 | Buy put wing, sell put ATM, sell call ATM, buy call wing (3 unique strikes) |
| Long iron butterfly | 4 | Sell put wing, buy put ATM, buy call ATM, sell call wing (3 unique strikes) |
| Long call condor | 4 | Buy 1, sell 1, sell 1, buy 1 across four ascending call strikes |
| Short iron condor | 4 | Buy put wing, sell put inner, sell call inner, buy call wing (4 unique strikes) |
| Long iron condor | 4 | Sell put wing, buy put inner, buy call inner, sell call wing (4 unique strikes) |
| Covered call (buy-write) | 2 | Stock leg ratio: 100 Buy + sell OTM call ratio: 1 |
| Married put | 2 | Stock leg ratio: 100 Buy + buy put ratio: 1 |
| Envelope shape | Recommended orderType |
|---|---|
Single-leg Option (any side) | Market or marketable Limit |
Mleg covered call (Buy 100 stock + Sell call, Open or Close) | Market |
Mleg married put (Buy 100 stock + Buy put, Open or Close) | Market |
Mleg 2-leg pure-option spreads - verticals (bull/bear, debit/credit), long/short straddle, long/short strangle | Market or Limit at your net debit/credit target |
Mleg 3-leg butterflies (call, put, 1-2-1 ratios) | Market or Limit |
Mleg 4-leg iron butterflies (3 unique strikes) | Market or Limit |
Mleg 4-leg condors with 4 unique strikes (long call condor, short iron condor, long iron condor) | Limit with a generous limitPrice (e.g. 1.00 for a long call condor debit, 0.10 for a short iron condor net) |
For any Mleg order, you can always send orderType: "Limit" with a limitPrice set to the net price you're willing to pay or accept - always a positive absolute value regardless of whether the spread is a debit or credit. Limit gives you explicit control over the spread's reservation price.
Not supported
The following shapes are not part of the strategy-template catalog. POST /order returns HTTP 400 with the detail text below in the JSON envelope:
| Shape | Reject reason (detail) |
|---|---|
| Calendar spread (same strike, different expirations) | Calendar orders are not currently supported. |
| Diagonal spread (different strikes and different expirations) | Calendar orders are not currently supported. (same template) |
Ratio spread (e.g. 1:2 vertical) | This strategy is not currently supported, please review the documentation for more information. |
| Box spread (4 legs across two strikes, both calls and puts) | This strategy is not currently supported... |
Common-divisor ratios (e.g. 2:2, simplifiable to 1:1) | Invalid order, leg ratios cannot have a common devisor. |
| Combinations that don't match a recognized template (e.g. 3 calls all in the same direction) | This strategy is not currently supported... |
| All-buy or all-sell legs (no offsetting direction) | This strategy is not currently supported... |
1 leg as Mleg | Too few legs, invalid order. (use securityType: "Option" instead) |
| 5+ legs | Too many legs, invalid order. |
Stock-leg ratio that isn't 100 (1, 50, 99) | This strategy is not currently supported... |
Other multi-leg combinations that don't match one of the templates in the Supported section above - including collars (long stock + long put + short call), synthetic long / short, risk reversal, and jade lizard - return one of the "This strategy is not currently supported..." detail strings.
The strategy detection is based purely on the shape of the legs:
- Number of legs
- Number of unique strikes
- Whether all legs share an expiration (calendar/diagonal otherwise)
- Whether all legs are the same option type (all calls or all puts) vs. mixed (calls + puts)
- Ratios (all
1, or butterfly's1-2-1, or the stock-leg's100)
Sending legs that don't match any recognized shape returns HTTP 400 with one of the detail strings above. Calendar/diagonal rejections take precedence over the general "strategy not supported" rejection.
Strategy examples
Vertical spread - bull call (debit)
Buy the lower strike call, sell the higher strike call at the same expiration. The limitPrice is the net debit you are willing to pay.
{
"securityType": "Mleg",
"symbol": "AAPL",
"orderType": "Limit",
"limitPrice": 2.50,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "bull-call-001",
"legs": [
{ "symbol": "AAPL260717C00600000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "AAPL260717C00700000", "side": "Sell", "ratio": 1, "openClose": "Open" }
]
}
Vertical spread - bear put (debit)
Buy the higher strike put, sell the lower strike put. The limitPrice is the net debit.
{
"securityType": "Mleg",
"symbol": "TSLA",
"orderType": "Limit",
"limitPrice": 3.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "bear-put-001",
"legs": [
{ "symbol": "TSLA260717P00200000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "TSLA260717P00150000", "side": "Sell", "ratio": 1, "openClose": "Open" }
]
}
Long straddle
Buy a call and a put at the same strike and expiration. Profits from a large move in either direction.
{
"securityType": "Mleg",
"symbol": "SPY",
"orderType": "Limit",
"limitPrice": 5.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "straddle-001",
"legs": [
{ "symbol": "SPY260717C00500000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260717P00500000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
Long strangle
Buy an OTM call and an OTM put at different strikes, same expiration.
{
"securityType": "Mleg",
"symbol": "AAPL",
"orderType": "Limit",
"limitPrice": 4.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "strangle-001",
"legs": [
{ "symbol": "AAPL260717C00700000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "AAPL260717P00500000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
Long call butterfly
A 3-leg, 1-2-1 ratio spread. Buy the outer strikes, sell 2 of the middle strike.
{
"securityType": "Mleg",
"symbol": "SPY",
"orderType": "Limit",
"limitPrice": 1.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "butterfly-001",
"legs": [
{ "symbol": "SPY260717C00450000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260717C00500000", "side": "Sell", "ratio": 2, "openClose": "Open" },
{ "symbol": "SPY260717C00550000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
Long call condor
A 4-leg spread across 4 consecutive strikes. Smaller profit window than a butterfly, lower cost.
{
"securityType": "Mleg",
"symbol": "AAPL",
"orderType": "Limit",
"limitPrice": 1.50,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "condor-001",
"legs": [
{ "symbol": "AAPL260717C00550000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "AAPL260717C00600000", "side": "Sell", "ratio": 1, "openClose": "Open" },
{ "symbol": "AAPL260717C00700000", "side": "Sell", "ratio": 1, "openClose": "Open" },
{ "symbol": "AAPL260717C00800000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
Iron butterfly (short)
Sell a straddle (ATM call + ATM put) and buy OTM wings on each side for defined risk. The two body legs share the same strike. limitPrice is the net credit received.
{
"securityType": "Mleg",
"symbol": "SPY",
"orderType": "Limit",
"limitPrice": 4.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "iron-butterfly-001",
"legs": [
{ "symbol": "SPY260717P00480000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260717P00500000", "side": "Sell", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260717C00500000", "side": "Sell", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260717C00520000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
The key constraint: legs[1].strike === legs[2].strike (both body legs share the ATM strike). The detectStrategy logic identifies an iron butterfly by exactly 4 legs with 3 unique strikes and mixed call/put types.
Iron condor (short)
Sell an OTM strangle (short call + short put at different strikes) and buy wider OTM wings on each side. The limitPrice is the net credit you require; the example below fills at a priceAvg of around -0.58 (the negative envelope-level price represents the net credit collected).
{
"securityType": "Mleg",
"symbol": "SPY",
"orderType": "Limit",
"limitPrice": 0.10,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "iron-condor-001-open",
"route": "PAPER",
"legs": [
{ "symbol": "SPY260522P00741000", "side": "Buy", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260522P00742000", "side": "Sell", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260522C00756000", "side": "Sell", "ratio": 1, "openClose": "Open" },
{ "symbol": "SPY260522C00757000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
To close the condor, send the same envelope with each side flipped and openClose: "Close" on every leg.
Leg order ascending by strike: buy put wing → sell put inner → sell call inner → buy call wing. All 4 strikes are unique. detectStrategy identifies an iron condor by exactly 4 legs with 4 unique strikes and mixed call/put types.
Covered call (buy-write)
Simultaneously buy 100 shares of stock and sell 1 OTM call to collect premium. The stock leg uses the bare ticker symbol and ratio: 100.
{
"securityType": "Mleg",
"symbol": "AAPL",
"orderType": "Limit",
"limitPrice": 195.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "covered-call-001",
"legs": [
{ "symbol": "AAPL", "side": "Buy", "ratio": 100, "openClose": "Open" },
{ "symbol": "AAPL260717C00600000", "side": "Sell", "ratio": 1, "openClose": "Open" }
]
}
The stock leg uses the bare underlying ticker ("AAPL") as its symbol, not an OCC symbol. The ratio must be 100 for the stock leg (representing 100 shares per contract). A ratio over 100 returns 400 from schema validation (- legs.0.ratio: Must be less than or equal to 100).
Married put (protective put)
Simultaneously buy 100 shares of stock and buy 1 put for downside protection.
{
"securityType": "Mleg",
"symbol": "AAPL",
"orderType": "Limit",
"limitPrice": 198.00,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "married-put-001",
"legs": [
{ "symbol": "AAPL", "side": "Buy", "ratio": 100, "openClose": "Open" },
{ "symbol": "AAPL260717P00150000", "side": "Buy", "ratio": 1, "openClose": "Open" }
]
}
Cash-secured put (CSP)
Sell (short) a put while reserving enough cash or buying power to cover the assignment cost (100 × strike) if the contract is exercised. There is no Mleg shape for this - the cash collateral is enforced by the broker against the account, not expressed as a leg in the order envelope. Send a normal single-leg securityType: "Option" order with side: "Sell" and openClose: "Open":
{
"securityType": "Option",
"symbol": "AAPL260717P00190000",
"side": "Sell",
"openClose": "Open",
"orderType": "Limit",
"limitPrice": 2.45,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "csp-aapl-put-001"
}
TradeZero evaluates whether the account has enough buying power to secure the put at the time of order placement (and again at fill), and reserves the collateral against the account automatically. The order envelope itself does not need a "collateral" field - send the single-leg STO and collateral rules apply on the broker side. Verify the account has sufficient buying power (/pnl) before submitting if you want to surface a check-out-side validation in your client.
To close a cash-secured put before expiration, send a Buy to Close on the same OCC symbol:
{
"securityType": "Option",
"symbol": "AAPL260717P00190000",
"side": "Buy",
"openClose": "Close",
"orderType": "Limit",
"limitPrice": 0.10,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "csp-aapl-put-close-001"
}
If the put is assigned at expiration, the assignment is handled outside the order endpoint - you'll see the resulting long stock position appear in GET /positions and a corresponding row in your trade history; no API call from you is required to "accept" the assignment.
A cash-secured put (collect premium, may end up assigned long stock at the strike) and a covered call (own stock, collect premium, may end up called away at the strike) are mirror income strategies. A common cycle ("the wheel"):
- Sell a cash-secured put at a strike you'd be happy to buy the stock at.
- If assigned, take delivery of the 100 shares.
- Sell a covered call at a strike you'd be happy to sell the stock at.
- If called away, you're flat - repeat from step 1.
Both legs of the wheel are placed through the API the same way as any other option order: the cash-secured put as a single-leg STO put (securityType: "Option"), and the covered call as either a single-leg STO call against existing stock or as a packaged Mleg covered call (Buy 100 stock + Sell 1 call) if you want to enter the long-stock and the short call atomically.
Validation errors
Schema 400 errors (plain text)
These are JSON-Schema validation failures returned with Content-Type: text/plain. The body is a hyphen-prefixed message string — read res.text() and branch on the Content-Type header before parsing.
| Condition | 400 body |
|---|---|
securityType missing, wrong case, or wrong value | - securityType: securityType must be one of the following: "Stock", "Option", "Mleg" |
symbol missing, empty, or fails character pattern (spaces, /, @, OSI long-form) | - symbol: Does not match pattern '^[a-zA-Z0-9._-]+$' |
side missing (single-leg) | Wrapper around - (root): side is required |
side wrong case (e.g. "buy") | - side: side must be one of the following: "Buy", "Sell" |
orderType missing, wrong case, or wrong value | - orderType: orderType must be one of the following: "Limit", "Market", "Stop", "StopLimit" |
timeInForce missing, wrong case, or wrong value (including "MarketOnClose" / "LimitOnClose") | - timeInForce: timeInForce must be one of the following: "Day", "GoodTillCancel", "AtTheOpening", "ImmediateOrCancel", "FillOrKill", "GoodTillCrossing", "Day_Plus", "GTC_Plus" |
orderQuantity missing or body unparseable | invalid character '<' looking for beginning of value |
orderQuantity fractional (e.g. 1.5) | - orderQuantity: Invalid type. Expected: integer, given: number |
orderQuantity <= 0 | - orderQuantity: Must be greater than or equal to 1 |
orderQuantity > 1,000,000 | - orderQuantity: Must be less than or equal to 1e+06 |
limitPrice > 9999.99 | - limitPrice: Must be less than or equal to 9999.99 |
securityType: "Mleg" with legs omitted or empty | - (root): legs is required |
securityType: "Mleg" with 1 leg | JSON envelope (see below): "Too few legs, invalid order." |
securityType: "Mleg" with 5+ legs | JSON envelope (see below): "Too many legs, invalid order." |
Root side present on a Mleg order | Schema validation error on side |
Leg ratio <= 0 (zero or negative) | - legs.0.ratio: Must be greater than or equal to 1 |
Leg ratio > 100 | - legs.0.ratio: Must be less than or equal to 100 |
Leg ratio fractional | - legs.0.ratio: Invalid type. Expected: integer, given: number |
Leg openClose missing, wrong case, or wrong value | - legs.0.openClose: legs.0.openClose must be one of the following: "Open", "Close" |
Leg side missing, wrong case, or wrong value | - legs.0.side: legs.0.side must be one of the following: "Buy", "Sell" |
openClose on single-leg ordersFor single-leg Option requests, always send "Open" or "Close" (properly cased) so order history records the correct trader action (BTO / STO / BTC / STC). On Mleg, openClose is required on every leg.
Strategy template errors (HTTP 400)
Some requests that pass JSON-schema validation are declined during strategy validation with HTTP 400 and a JSON body. These come back with Content-Type: application/json and a statusCode/message/detail envelope:
{
"statusCode": "BadRequest",
"message": "PlaceOrderWithResponse",
"detail": "This strategy is not currently supported, please review the documentation for more information."
}
| Condition | detail field (verbatim) |
|---|---|
Unsupported strategy shape (collar, synthetic, ratio spread, box spread, all-buy or all-sell legs, stock-leg ratio that isn't 100, or any combination not matching one of the templates in Supported) | "This strategy is not currently supported, please review the documentation for more information." |
| Calendar or diagonal spread (any legs with different expiration dates) | "Calendar orders are not currently supported." |
Too many legs (> 4) | "Too many legs, invalid order." |
Too few legs (Mleg with 1 leg) | "Too few legs, invalid order." |
Leg ratios share a common divisor > 1 (e.g., 2:2, simplifiable to 1:1) | "Invalid order, leg ratios cannot have a common devisor." |
Rolling (openClose: "Close" on a leg) without a matching open position to unwind | "Mleg Order is not compatible with current position." |
The stock leg in a covered call or married put must use ratio: 100 (representing 100 shares per spread unit), with orderQuantity: 1 representing the number of spread units. Sending ratio: 1, 50, or 99 with orderQuantity: 100 returns HTTP 400 with "This strategy is not currently supported...". Ratios > 100 fail schema validation directly with - legs.0.ratio: Must be less than or equal to 100.
Post-submission rejections (HTTP 200, orderStatus: "Rejected")
Some requests are accepted (HTTP 200, orderStatus: "PendingNew") and then return orderStatus: "Rejected" with a populated text field carrying an R##: reason code. Common reason codes:
| Trigger | text |
|---|---|
Negative limitPrice or stopPrice | "R145: Negative Price Is Not Allowed" |
Market / Stop order with no live reference price (illiquid contract) | "R88: Orders cannot have a reference price of 0.00" |
| Sell to Close without a matching open position (the order is evaluated as a naked short under the account's option level rules) | "R00: NakedCall is not permitted at OptionLevel 3" (the level shown matches the account's optionTradingLevel) |
Mleg envelope symbol (underlying) doesn't match the leg OCC underlyings | "R152: Validation Order Error: Malformed Multi Leg Order - Leg And Root Symbol Mismatch" |
| Mleg leg can't be priced (no available quote for the strike at the time of submission) | "UMC: Could not obtain pricing on the order for leg <OCC_SYMBOL> WhileProcessing: <accountId>:<clientOrderId>" |
4-unique-strike Mleg condor sent as Market with no synthesizable net price | The order returns with text: null, limitPrice: 0, legCount: 0, legs: null, openClose: "Unknown". Resubmit the same legs with orderType: "Limit" and a limitPrice set to your target net debit/credit. |
Single-leg Option submitted on route: "PAPERM" (Mleg-only paper route) | "R54: Unable to reach the destination Route" |
For asynchronously rejected orders the route field echoes the route the request was submitted on (e.g. "PAPER", "PAPERM") - read orderStatus and text to detect a rejection, not the route value.
Routes for options
GET /v1/api/accounts/{accountId}/routes
The routes response lists which routing destinations support option and multi-leg orders via the securityTypes field on each route.
{
"routes": [
{
"routeName": "PAPER",
"securityTypes": ["Stock", "Option"],
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "RangeOrder"],
"timesInForce": ["Day", "GoodTillCancel", "GoodTillCrossing"],
"useDisplayQty": false
},
{
"routeName": "PAPERM",
"securityTypes": ["MLEG"],
"orderTypes": ["Market", "Limit", "Stop", "StopLimit", "RangeOrder"],
"timesInForce": ["Day"],
"useDisplayQty": false
}
]
}
{
"routes": [
{
"routeName": "SMARTO",
"securityTypes": ["Option"],
"orderTypes": ["Market", "Limit", "Stop", "StopLimit"],
"timesInForce": ["Day", "GoodTillCancel"],
"useDisplayQty": false
},
{
"routeName": "SMARTM",
"securityTypes": ["Option", "MLEG"],
"orderTypes": ["Market", "Limit"],
"timesInForce": ["Day"],
"useDisplayQty": false
}
]
}
On a live account, SMARTO routes single-leg option orders and SMARTM routes multi-leg spreads (MLEG). Route single-leg orders through SMARTO and multi-leg orders through SMARTM. The route set on a given live account depends on your subscription - query /routes and use what it returns rather than hard-coding a list.
On paper, the route field is echoed back as sent. When route is omitted, the API assigns a default ("PAPER" for stock-included envelopes, "PAPERM" for pure-option Mleg envelopes). You can send the same route names you use on live ("SMARTO", "SMARTM") and the paper response will echo them back, which keeps order-construction code shared between environments.
route for the simplest pathOn a paper account, the simplest path is to omit route entirely on Mleg orders — the API assigns a paper destination automatically ("PAPERM" for pure-option Mleg, "PAPER" for stock-included Mleg). Sending "PAPER" or "PAPERM" explicitly is also accepted and echoed back.
On live accounts, send an explicit route from /routes (SMARTM for MLEG, SMARTO for single-leg options). Omitting route on live can leave the order with no route assigned and produce R54: Unable to reach the destination Route.
PAPERM is Mleg-onlyOn paper, send single-leg Option orders without route (or with route: "PAPER") and Mleg orders without route (or with route: "PAPER" / "PAPERM"). Single-leg Option orders routed through PAPERM return orderStatus: "Rejected" with text: "R54: Unable to reach the destination Route" — PAPERM is reserved for MLEG.
To filter routes relevant to options orders, check whether the securityTypes array contains "option" or "mleg" using a case-insensitive comparison. The values are returned as mixed-case ("Option", "MLEG") on both paper and live, so normalize before comparing:
/** Returns routes valid for single-leg options or multi-leg spreads. */
function filterOptionsRoutes(routes: TZRoute[]): TZRoute[] {
return routes.filter(r =>
r.securityTypes?.some(st =>
st.toLowerCase() === 'option' || st.toLowerCase() === 'mleg'
) ?? false
)
}
Both "Option" and "MLEG" securities appear in the same filtered list. A route that appears only with "MLEG" (like PAPERM) does not support single-leg options and vice versa.
Mleg routes are typically Day-only - the paper PAPERM route advertises ["Day"], and the live SMARTM route also advertises ["Day"]. If you need a longer-lived multi-leg order, you'll need to re-submit the next session. Single-leg options on live (SMARTO) accept Day and GoodTillCancel. Always defer to what /routes advertises for your specific account rather than hard-coding TIFs.
Options in order history
GET /orders returns option order rows with several additional fields compared to equity rows:
| Field | Type | Notes |
|---|---|---|
securityType | string | "Option" or "MLEG" (uppercase) |
symbol | string | Underlying ticker ("AAPL", "SPY") - not the OCC symbol |
tradedSymbol | string | null | Full OCC symbol for single-leg; underlying ticker for MLEG; null for rejected orders |
putCall | string | "Call", "Put", or "None" (for MLEG rows) |
strikePrice | number | Strike price (e.g. 600); 0 for rejected orders and Mleg envelope rows |
mlegID | number | Non-zero ID shared across all leg rows of the same Mleg order. 0 for single-leg orders. |
legIndex | number | -1 for single-leg and Mleg envelope rows. Set to leg position index on individual leg fill rows. |
dtOptExpire | string | Option expiration timestamp. Defaults to "0001-01-01T00:00:00Z" when not populated. |
root | string | Root symbol (ticker root without suffix). Often empty. |
priceRootSym | number | Last price of the underlying at time of order. |
The symbol field in GET /orders always returns the underlying ticker for option orders - use tradedSymbol to get the full OCC symbol.
GET /orders/start-date/{startDate} returns fill-level rows (not order rows) and has a much smaller field set: tradeId, accountId, symbol (the ticker traded - for options this is the underlying root), securityType ("Option" for single-leg, may differ for multi-leg fills), side, qty, price, commission, totalFees, grossProceeds, netProceeds, tradeDate, settleDate, entryDate, execTime, canceled, mLegId (lowercase m, set to a non-zero spread ID on multi-leg fills), spreadType, notes, currency. Order-level option fields like tradedSymbol, putCall, strikePrice, dtOptExpire, legIndex, and root are not carried on these per-fill rows - to recover those, look up the originating order in GET /orders (within the same session) or persist them client-side from your POST /order response.
Options positions
GET /v1/api/accounts/{accountId}/positions returns option positions alongside stock positions. Filter by securityType === "Option" to isolate them. Each individual leg of a filled Mleg envelope shows up as its own row keyed by tradedSymbol - the spread is decomposed into per-leg lots, not aggregated.
{
"accountId": "TZP12345678",
"createdDate": "2026-05-14T16:35:58.358+00:00",
"dayOvernight": "Day",
"maintenanceRequirement": 0,
"marginRequirement": 0,
"positionId": "2260514163558358578",
"priceAvg": 6.22,
"priceClose": 0,
"priceOpen": 6.22,
"priceStrike": 748,
"putCall": "Put",
"rootSymbol": null,
"securityType": "Option",
"shares": 1,
"side": "Long",
"symbol": "SPY",
"tradedSymbol": "SPY260522P00748000",
"updatedDate": "2026-05-14T16:35:58.358+00:00"
}
{
"accountId": "TZP12345678",
"positionId": "2260514163553652569",
"securityType": "Stock",
"symbol": "SPY",
"tradedSymbol": null,
"shares": 100,
"side": "Long",
"priceAvg": 747.78,
"priceOpen": 747.78,
"priceClose": 0,
"priceStrike": 0,
"putCall": "None",
"dayOvernight": "Day",
"rootSymbol": null
}
| Field | Notes |
|---|---|
securityType | "Option" for option positions; "Stock" for the stock leg of a covered call / married put |
symbol | Underlying ticker ("SPY") - same for stock leg and option leg |
tradedSymbol | Full OCC symbol ("SPY260522P00748000") for option legs; null for stock legs |
priceStrike | Strike price as a number for option legs; 0 for stock legs |
putCall | "Call" or "Put" for option legs; "None" for stock legs |
shares | Positive for long; negative for short. Stock legs use share count; option legs use contract count (1 contract = 100 shares of underlying) |
side | "Long" or "Short" |
priceAvg | Average cost basis (premium paid/received per share for option legs; per-share price for stock legs) |
priceClose | 0 while the position is open; populated only after the position is fully closed |
dayOvernight | "Day" for positions opened today |
rootSymbol | Reserved field; commonly null |
positionId | TradeZero-assigned lot ID - distinct per leg, even for legs that came from the same Mleg envelope |
Closing a single-leg option position
To close a long option, send side: "Sell", openClose: "Close" with the OCC symbol as symbol. To close a short option, send side: "Buy", openClose: "Close".
Match the position using tradedSymbol (the full OCC symbol), not the underlying symbol field.
function buildCloseOptionOrder(
pos: TZPosition,
quote: { bid: number; ask: number },
extendedHours = false,
): TZPlaceOrderRequest {
const isLong = pos.shares > 0
return {
securityType: 'Option',
symbol: pos.tradedSymbol ?? pos.symbol, // Use the OCC symbol from tradedSymbol
side: isLong ? 'Sell' : 'Buy',
openClose: 'Close',
orderQuantity: Math.abs(pos.shares),
// During extended hours: Limit+Day_Plus. During regular hours: Market+Day is acceptable.
orderType: extendedHours ? 'Limit' : 'Market',
limitPrice: extendedHours
? (isLong ? quote.bid : quote.ask) // Marketable limit: sell at bid, buy at ask
: undefined,
timeInForce: extendedHours ? 'Day_Plus' : 'Day',
clientOrderId: `close-${pos.tradedSymbol}-${Date.now()}`,
}
}
There is no separate "close spread" endpoint. To unwind a multi-leg position, construct a new Mleg order with the same legs but reversed sides (legs that were "Buy" become "Sell" and vice versa) and openClose: "Close" on each leg. See Closing a multi-leg position above for a worked example.
The mlegID returned on the open envelope's filled GET /order/{clientOrderId} response is also returned on the close envelope — clients can match opens with closes by grouping on mlegID.
Integration patterns
Building the OCC symbol
The buildOccSymbol helper from the OCC symbol format section is what you call before each order. The components come from whatever option chain data source you use - the TradeZero API does not provide an options chain endpoint.
// AAPL call, Jul 17 2026, $600 strike → "AAPL260717C00600000"
buildOccSymbol('AAPL', new Date('2026-07-17'), 'Call', 600)
// SPY put, Jan 15 2027, $450.50 strike → "SPY270115P00450500"
buildOccSymbol('SPY', new Date('2027-01-15'), 'Put', 450.50)
Parsing the OCC symbol
To decode an OCC symbol back to its components (useful when displaying tradedSymbol from order history):
const OCC_REGEX = /^([A-Z0-9]{1,6})(\d{6})([CP])(\d{8})$/
interface ParsedOcc {
underlying: string
expiration: Date
callPut: 'Call' | 'Put'
strike: number
}
function parseOccSymbol(occ: string): ParsedOcc | null {
const m = OCC_REGEX.exec(occ)
if (!m) return null
const [, underlying, dateStr, cp, strikeStr] = m
const yy = parseInt(dateStr.slice(0, 2), 10)
const mm = parseInt(dateStr.slice(2, 4), 10) - 1
const dd = parseInt(dateStr.slice(4, 6), 10)
return {
underlying,
expiration: new Date(2000 + yy, mm, dd),
callPut: cp === 'C' ? 'Call' : 'Put',
strike: parseInt(strikeStr, 10) / 1000,
}
}
Grouping Mleg order history by spread
GET /orders returns one row per envelope (not one row per leg) when the order is still session-visible. Each Mleg envelope row can carry a non-zero mlegID shared with the matching close envelope. Group by mlegID to pair each open with its close — for filled orders that have dropped from the list, use GET /order/{clientOrderId} instead:
function groupByMlegId(orders: TZOrder[]): Map<number, TZOrder[]> {
const groups = new Map<number, TZOrder[]>()
for (const o of orders) {
if (!o.mlegID || o.mlegID === 0) continue
const group = groups.get(o.mlegID) ?? []
group.push(o)
groups.set(o.mlegID, group)
}
return groups
}
For example, the married put round-trip shown in the Mleg response section returns mlegID: 108908 on both the open envelope (Buy stock + Buy put, openClose: "Open") and the matching close envelope (Sell stock + Sell put, openClose: "Close").
Working with the option order response
The top-level fields of an option order share the same clean REST shape across POST /order, GET /orders, and GET /order/{clientOrderId} — see Trading → Order shape across REST and WebSocket. Portfolio WebSocket Order pushes and filled Mleg GET /order/{clientOrderId} responses use the WebSocket field set. For options specifically:
symbolis always the underlying ticker (e.g."AAPL").tradedSymbolis the OCC contract for single-leg options (e.g."AAPL260717C00600000"); forMlegorders it equals the underlying ticker.securityTypeechoes as"Option"for single-leg orders and"MLEG"(uppercase) for the multi-leg envelope row, even though the request uses"Mleg"(mixed case).GET /order/{clientOrderId}returns the clean field shape for single-leg / stock orders and for Mleg orders beforelegs[]is populated, and switches to the WebSocket field shape (account,userOrderId,cancelledQuantity,lastQty,maxDisplayQty,mlegID) oncelegs[]is populated. Normalize both shapes if your client reads filled Mleg orders this way - see the Mleg response section for the before/after example.
Mleg leg field names: The per-leg objects inside the legs[] array use wire-layer field names that differ from the top-level order fields. If you bridge legs into a unified data model, the common mapping is:
Wire field on legs[] | Common normalized name | Notes |
|---|---|---|
qty | ratio | Per-leg contract count |
avgPx | priceAvg | Average fill price per leg |
lastQty | executed | Last fill quantity per leg |
lastPx | lastPrice | Last fill price per leg |
lvsQty | leavesQuantity | Remaining unfilled per leg |
cxlQty | canceledQuantity | Canceled quantity per leg |
function normalizeMlegLegs(legs: unknown[]): NormalizedLeg[] {
return legs.map((leg: any) => ({
...leg,
ratio: leg.ratio ?? leg.qty ?? 1,
priceAvg: leg.priceAvg ?? leg.avgPx ?? 0,
executed: leg.executed ?? leg.lastQty ?? 0,
lastPrice: leg.lastPrice ?? leg.lastPx ?? 0,
leavesQuantity: leg.leavesQuantity ?? leg.lvsQty ?? 0,
canceledQuantity: leg.canceledQuantity ?? leg.cxlQty ?? 0,
}))
}
FAQs
Do I need to specify openClose per leg for Mleg orders?
Yes. Each leg object requires openClose: "Open" or "Close". Wrong values return 400 from schema validation. The root-level openClose field is not used for Mleg orders.
Why does my Mleg order return 400 with "This strategy is not currently supported"?
There are two main causes:
-
The leg shape doesn't match any recognized template. Check that your combination matches one of the supported shapes - verticals (debit or credit), straddle (long or short), strangle (long or short), butterfly (1-2-1 ratios), iron butterfly (4 legs, 3 unique strikes), condor, iron condor (4 legs, 4 unique strikes), covered call, or married put. Also check: all-buy or all-sell leg combinations are rejected, as are legs where ratios share a common divisor (e.g.,
2:2returns"Invalid order, leg ratios cannot have a common devisor."). -
Stock leg structure for covered call / married put. The stock leg must use
ratio: 100withorderQuantity: 1. Usingratio: 1,50, or99withorderQuantity: 100returnsHTTP 400during strategy validation.
Iron condor specifically: iron condor (long and short) is accepted on the paper PAPER and PAPERM routes (and on SMARTM on live accounts). Send orderType: "Limit" with limitPrice set to the net price you'll accept - always a positive absolute value, whether the strategy is a debit (long iron condor) or a credit (short iron condor). See the recommended order types per strategy table for guidance per shape.
What is the maximum number of legs?
Four. A 5-leg order returns 400 with "Too many legs, invalid order.". A 1-leg Mleg returns 400 with "Too few legs, invalid order." - use securityType: "Option" for single-leg orders.
Can I send side at the root level of a Mleg order?
No. Remove root side from Mleg envelopes entirely — direction is expressed per leg via each leg's side field.
Why does the response symbol field show the underlying ticker, not the OCC symbol?
The symbol field in both POST /order responses and GET /orders rows always returns the underlying ticker. The full OCC symbol is in tradedSymbol. For rejected orders, tradedSymbol is null.
Can I use GTC or extended-hours TIFs on Mleg orders?
On paper accounts, the PAPERM route only advertises "Day" as a supported TIF for Mleg orders. On live accounts, the available TIFs depend on your routing configuration - query GET /routes and check the timesInForce list for the route that handles "MLEG" securityTypes.
Does the TradeZero API provide option chains, expirations, or greeks?
No. The TradeZero REST API does not expose an options chain, expiry lookup, or greeks endpoint. Expirations, strikes, and greeks must be sourced from a market data provider.
Can I mix different expirations in a single Mleg order (calendar spread)?
No. Calendar spreads are not supported. Sending legs with different expiration dates returns 400 with "Calendar orders are not currently supported.".
How do I sell a cash-secured put?
Send a normal single-leg securityType: "Option" order with side: "Sell" and openClose: "Open" - there is no special envelope for cash-secured puts. TradeZero evaluates whether your account has enough cash or buying power to secure the assignment cost (100 × strike × orderQuantity) at the time of the order; if buying power is insufficient, the order returns orderStatus: "Rejected" with a buying-power-related text reason. See Cash-secured put (CSP) for an example.
What option approval level do I need for these strategies?
Approval levels are surfaced on the account row as optionTradingLevel (and the alias optLevel) - see Account Information. The standard 0-4 scale roughly maps to:
- Level 0 - options not enabled.
- Level 1 - covered calls, cash-secured puts, protective puts (long puts against held stock).
- Level 2 - long calls, long puts (premium-only debit positions).
- Level 3 - multi-leg debit and credit spreads (verticals, straddles, strangles, butterflies, condors, iron variants).
- Level 4 - uncovered (naked) short calls and short puts without cash collateral.
Branch on optionTradingLevel before sending any option order to confirm the account is permissioned for the strategy you're about to send. The exact level required for a given strategy can vary by account configuration - when in doubt, query GET /account/{accountId} and confirm the optionTradingLevel value before submitting.
Additional resources
- Equity Trading - auth headers,
clientOrderIdrules, cancel patterns, order lifecycle, and error handling (all apply to options too). - Open Positions - reading and calculating P&L on option positions.
- Account Information - the
optLevelandoptionTradingLevelfields show the account's options approval level. - Portfolio WebSocket - real-time order state pushes, including option and Mleg order updates.
- API Reference - POST /order - auto-generated OpenAPI reference.
- API Reference - GET /orders - today's orders blotter including option and Mleg rows.
- API Reference - GET /routes - list routes available for your account before submitting option orders.
Recipes
- OCC Option Symbol - build and parse the compact OCC string the API expects on every option order.
- Multi-Leg Options Spread - submit a vertical spread end-to-end, including leg sorting and the
Mlegenvelope rules. - Options Strategy Cookbook - request bodies for every supported strategy (verticals, straddles, strangles, butterflies, condors, iron variants, covered calls, married puts, cash-secured puts).
- Close Option Position - close out an existing short option position with a
Buy + Closesingle-leg order.