Skip to content
Skip to main content

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

MethodPathPurpose
GET/v1/api/accounts/{accountId}/routesList routing destinations - filter by securityType containing "Option" or "MLEG"
GET/v1/api/accounts/{accountId}/ordersToday'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}/orderPlace 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/ordersCancel all open orders (optionally scoped to a symbol)
GET/v1/api/accounts/{accountId}/positionsOpen 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}
ComponentLengthExample
Underlying ticker1-6 charsAAPL
Expiration: YYMMDD6 digits260717 = Jul 17 2026
Put/Call flag1 charC = Call, P = Put
Strike × 1000, padded to 8 digits8 digits00600000 = $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:

ActionsideopenCloseDescription
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

FieldTypeRequiredNotes
securityTypestringYes"Option". Case-sensitive - "option" returns 400 with - securityType: securityType must be one of the following: "Stock", "Option", "Mleg".
symbolstringYesThe full OCC option symbol, e.g. "AAPL260717C00600000". Same character pattern as equities (^[a-zA-Z0-9._-]+$).
sidestringYes"Buy" or "Sell". Case-sensitive.
openClosestringRecommended"Open" or "Close". Always send properly cased "Open" or "Close" so order history records the correct trader action (BTO / STO / BTC / STC).
orderTypestringYes"Market", "Limit", "Stop", or "StopLimit". Case-sensitive.
orderQuantityintegerYesNumber 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.
timeInForcestringYesOne of the eight schema-valid values in the TIF table.
limitPricenumberConditionalRequired 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".
stopPricenumberConditionalRequired 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.
clientOrderIdstringRecommendedYour identifier for deduplication and cancel-by-id. Keep ≤ 36 characters for live-exchange compatibility.
routestringRecommendedRouting 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)

Buy to open 1 AAPL call at limit (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": "Option",
"symbol": "AAPL260717C00600000",
"side": "Buy",
"openClose": "Open",
"orderType": "Limit",
"limitPrice": 3.50,
"orderQuantity": 1,
"timeInForce": "Day",
"clientOrderId": "bto-aapl-call-001"
}'

Response (single-leg)

Order accepted - PendingNew (paper account)
{
"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)

FieldNotes
symbolThe underlying ticker (e.g. "AAPL"), not the full OCC symbol.
tradedSymbolThe full OCC symbol (e.g. "AAPL260717C00600000") for accepted orders. May be null on certain early-rejection rows.
strikePriceThe strike price as a number (e.g. 600). Populated for accepted orders; may be 0 on rejected rows.
securityTypeEchoed as "Option" for single-leg orders.
openCloseEchoes the value sent. The server may surface "Unknown" on certain early-rejection paths where it could not parse the trader action.
legCount0 for single-leg orders.
legsnull 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:

timeInForcePAPER (single-leg paper)PAPERM (Mleg paper)SMARTO (single-leg live)SMARTM (Mleg live)
DayAdvertised + acceptedAdvertised + acceptedAdvertised + acceptedAdvertised + accepted
GoodTillCancelAdvertised + acceptedAccepted on paperAdvertised + acceptedAccepted on live
GoodTillCrossingAdvertised + acceptedAccepted on paperAccepted on liveAccepted on live
Day_PlusAccepted on paperAccepted on paperUse a route that advertises itUse a route that advertises it
GTC_PlusAccepted on paperAccepted on paperUse a route that advertises itUse a route that advertises it
AtTheOpeningAccepted on paperAccepted on paperUse a route that advertises itUse a route that advertises it
ImmediateOrCancelAccepted on paperAccepted on paperUse a route that advertises itUse a route that advertises it
FillOrKillAccepted on paperAccepted on paperUse a route that advertises itUse 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.

Always defer to /routes

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

  1. securityType must be "Mleg" - not "mleg" or "MLEG" (returns 400 if wrong case).
  2. symbol must be the underlying ticker (e.g. "AAPL"), not an OCC symbol. The envelope symbol and every leg's underlying must match - sending different underlyings across legs is accepted at schema and asynchronously rejected at HTTP 200 with text: "R152: Validation Order Error: Malformed Multi Leg Order - Leg And Root Symbol Mismatch".
  3. No side at the root level. Mleg envelopes express direction per leg only — omit root side. If present, the API returns 400 with a schema validation error.
  4. Each leg in legs[] carries its own side, ratio, and openClose - all three are schema-required (unlike single-leg, where openClose is not enforced).
  5. Minimum 2 legs; maximum 4 legs. 1 leg returns "Too few legs, invalid order."; 5+ legs return "Too many legs, invalid order.".
  6. Leg ratio must be an integer between 1 and 100 inclusive. The stock leg of covered call / married put must be exactly 100.
  7. All legs must share the same expiration. Mixed expirations return "Calendar orders are not currently supported.".
  8. Leg ratios must not share a common divisor > 1. 2:2 returns "Invalid order, leg ratios cannot have a common devisor."; reduce to 1:1.

Request fields - envelope

FieldTypeRequiredNotes
securityTypestringYes"Mleg". Case-sensitive.
symbolstringYesThe underlying ticker, e.g. "AAPL" or "SPY". Always uppercase.
side-OmitMust not appear at the root level. Each leg carries its own side.
orderTypestringYes"Market", "Limit", "Stop", or "StopLimit".
orderQuantityintegerYesNumber of spread units (contracts).
limitPricenumberConditionalThe 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".
stopPricenumberConditionalRequired for "Stop" and "StopLimit". If omitted for "StopLimit", the limitPrice value is used as a fallback.
timeInForcestringYesDay is the only TIF advertised by both the paper PAPERM and live SMARTM Mleg routes.
clientOrderIdstringOptionalUsed 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.
routestringRecommendedRouting 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.
legsarrayYesBetween 2 and 4 leg objects.
limitPrice convention

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

FieldTypeRequiredNotes
symbolstringYesOCC 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").
sidestringYes"Buy" or "Sell" for this specific leg.
ratiointegerYesContract ratio. 1 for most strategies; 2 for the body legs of a butterfly. Range: 1-100.
openClosestringYes"Open" or "Close". Schema-validated per leg - wrong value returns HTTP 400.
Leg sort order

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)

Bull call spread (vertical debit spread)
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:

  1. Clean shape — returned by POST /order, GET /orders (each row), and GET /order/{clientOrderId} while the order is PendingNew, terminal-Rejected, or otherwise before legs[] is populated. Field names match the single-leg / stock order shape: accountId, clientOrderId, canceledQuantity, lastQuantity, maxDisplayQuantity.
  2. WebSocket shape — returned by GET /order/{clientOrderId} once legs[] is populated (i.e. for Filled, PartiallyFilled, or any state that has crossed into the spread book), and by Portfolio WebSocket Order pushes. Field names use account, userOrderId, cancelledQuantity (double-l), lastQty, maxDisplayQty, plus mlegID, 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:

FieldNotes
securityTypeEchoed as "MLEG" (uppercase), even though the request uses "Mleg"
symbolThe underlying ticker (e.g. "SPY")
tradedSymbolAlso the underlying ticker for Mleg orders
sideReflects 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
legCountNumber of legs. 0 on the immediate POST response and on rejected envelopes; populated to 2/3/4 once the spread is processed
legsnull until the spread is processed; then an array of leg execution details
limitPriceFor 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:

FieldDescription
indexZero-based leg position in the envelope
symbolOCC option symbol for option legs; underlying ticker for the stock leg of covered call / married put
side"Buy" or "Sell"
qty / ratioPer-leg contract count (or share count for the stock leg). Wire field is qty; normalize to ratio
lvsQtyLeaves quantity - remaining unfilled contracts
lastQtyLast fill quantity
lastPxLast fill price
avgPxAverage fill price for this leg. Normalize to priceAvg
cxlQtyCanceled 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):

POST /order - married put, immediate response (clean shape, legs not yet populated)
{
"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"
}
GET /order/{clientOrderId} - same order after fill (WebSocket shape, populated legs[])
{
"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"
}
Same envelope, two shapes

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:

POST /order - closing the married put (Sell stock + Sell put, openClose: Close)
{
"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" }
]
}
GET /order/{clientOrderId} — filled close (WebSocket shape, with mlegID linking open + 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:

GET /order/{clientOrderId} - bull-call vertical OPEN, after fill (WebSocket shape, populated legs[])
{
"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"
}
GET /order/{clientOrderId} - bull-call vertical CLOSE, after fill
{
"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.

Choosing orderType for Mleg orders
  • Market is 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.
  • Limit with a limitPrice set 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, Limit with a generous net price (e.g. 1.00 for a long call condor debit, 0.10 for 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"):

StrategyLegsDirection
Bull call spread (vertical, debit)2Buy lower-strike call, sell higher-strike call
Bear put spread (vertical, debit)2Buy higher-strike put, sell lower-strike put
Bull put spread (vertical, credit)2Sell higher-strike put, buy lower-strike put
Bear call spread (vertical, credit)2Sell lower-strike call, buy higher-strike call
Long straddle2Buy call + buy put at the same strike
Short straddle2Sell call + sell put at the same strike
Long strangle2Buy OTM call + buy OTM put at different strikes
Short strangle2Sell OTM call + sell OTM put at different strikes
Long call butterfly3Buy 1 low, sell 2 mid, buy 1 high (ratio 1-2-1, calls)
Long put butterfly3Buy 1 high, sell 2 mid, buy 1 low (ratio 1-2-1, puts)
Short iron butterfly4Buy put wing, sell put ATM, sell call ATM, buy call wing (3 unique strikes)
Long iron butterfly4Sell put wing, buy put ATM, buy call ATM, sell call wing (3 unique strikes)
Long call condor4Buy 1, sell 1, sell 1, buy 1 across four ascending call strikes
Short iron condor4Buy put wing, sell put inner, sell call inner, buy call wing (4 unique strikes)
Long iron condor4Sell put wing, buy put inner, buy call inner, sell call wing (4 unique strikes)
Covered call (buy-write)2Stock leg ratio: 100 Buy + sell OTM call ratio: 1
Married put2Stock leg ratio: 100 Buy + buy put ratio: 1
Recommended order types per strategy
Envelope shapeRecommended 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 strangleMarket 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:

ShapeReject 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 MlegToo few legs, invalid order. (use securityType: "Option" instead)
5+ legsToo 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's 1-2-1, or the stock-leg's 100)

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.

Bull call spread request body
{
"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.

Bear put spread request body
{
"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.

Long straddle request body
{
"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.

Long strangle request body
{
"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.

Long call butterfly request body
{
"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.

Long call condor request body
{
"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.

Short iron butterfly request body
{
"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).

Short iron condor request body - open
{
"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.

Covered call (buy-write) request body
{
"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" }
]
}
Covered call / married put leg structure

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.

Married put request body
{
"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":

Cash-secured put - Sell to Open 1 AAPL put at limit
{
"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:

Closing the cash-secured put - Buy to Close
{
"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.

Covered call ↔ cash-secured put: two halves of the "wheel"

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

  1. Sell a cash-secured put at a strike you'd be happy to buy the stock at.
  2. If assigned, take delivery of the 100 shares.
  3. Sell a covered call at a strike you'd be happy to sell the stock at.
  4. 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.

Condition400 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 unparseableinvalid 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 legJSON envelope (see below): "Too few legs, invalid order."
securityType: "Mleg" with 5+ legsJSON envelope (see below): "Too many legs, invalid order."
Root side present on a Mleg orderSchema 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"
Always send openClose on single-leg orders

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

Unsupported strategy rejection
{
"statusCode": "BadRequest",
"message": "PlaceOrderWithResponse",
"detail": "This strategy is not currently supported, please review the documentation for more information."
}
Conditiondetail 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."
Covered call / married put ratio rule

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:

Triggertext
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 priceThe 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.

Paper account routes (options-relevant)
{
"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
}
]
}
Live account routes (options-relevant)
{
"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.

Paper accounts and explicit route names

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.

Paper accounts: omit route for the simplest path

On 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-only

On 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 TIF restrictions

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:

FieldTypeNotes
securityTypestring"Option" or "MLEG" (uppercase)
symbolstringUnderlying ticker ("AAPL", "SPY") - not the OCC symbol
tradedSymbolstring | nullFull OCC symbol for single-leg; underlying ticker for MLEG; null for rejected orders
putCallstring"Call", "Put", or "None" (for MLEG rows)
strikePricenumberStrike price (e.g. 600); 0 for rejected orders and Mleg envelope rows
mlegIDnumberNon-zero ID shared across all leg rows of the same Mleg order. 0 for single-leg orders.
legIndexnumber-1 for single-leg and Mleg envelope rows. Set to leg position index on individual leg fill rows.
dtOptExpirestringOption expiration timestamp. Defaults to "0001-01-01T00:00:00Z" when not populated.
rootstringRoot symbol (ticker root without suffix). Often empty.
priceRootSymnumberLast 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.

Option position row - example after a married put fill
{
"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"
}
Stock leg of the same married put - also a separate /positions row
{
"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
}
FieldNotes
securityType"Option" for option positions; "Stock" for the stock leg of a covered call / married put
symbolUnderlying ticker ("SPY") - same for stock leg and option leg
tradedSymbolFull OCC symbol ("SPY260522P00748000") for option legs; null for stock legs
priceStrikeStrike price as a number for option legs; 0 for stock legs
putCall"Call" or "Put" for option legs; "None" for stock legs
sharesPositive 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"
priceAvgAverage cost basis (premium paid/received per share for option legs; per-share price for stock legs)
priceClose0 while the position is open; populated only after the position is fully closed
dayOvernight"Day" for positions opened today
rootSymbolReserved field; commonly null
positionIdTradeZero-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()}`,
}
}
Multi-leg position close

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:

  • symbol is always the underlying ticker (e.g. "AAPL").
  • tradedSymbol is the OCC contract for single-leg options (e.g. "AAPL260717C00600000"); for Mleg orders it equals the underlying ticker.
  • securityType echoes 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 before legs[] is populated, and switches to the WebSocket field shape (account, userOrderId, cancelledQuantity, lastQty, maxDisplayQty, mlegID) once legs[] 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 nameNotes
qtyratioPer-leg contract count
avgPxpriceAvgAverage fill price per leg
lastQtyexecutedLast fill quantity per leg
lastPxlastPriceLast fill price per leg
lvsQtyleavesQuantityRemaining unfilled per leg
cxlQtycanceledQuantityCanceled 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:

  1. 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:2 returns "Invalid order, leg ratios cannot have a common devisor.").

  2. Stock leg structure for covered call / married put. The stock leg must use ratio: 100 with orderQuantity: 1. Using ratio: 1, 50, or 99 with orderQuantity: 100 returns HTTP 400 during 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

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 Mleg envelope 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 + Close single-leg order.