Skip to content
Skip to main content

Trading API FAQs

Answers to frequent questions about the TradeZero Trading API. For detailed technical documentation, see the relevant reference page linked in each answer.


API program & agreement

What kind of software can I build with the API?

The API Trading program is for self-directed traders building personal assistive tooling on their own TradeZero account - dashboards, hotkey helpers, execution assistants, and integrations you run for yourself. You keep full GUI access through ZeroPro, TZ1, or ZeroMobile alongside your integration. Fully unattended systems that trade without your ongoing control are outside the scope of the current agreement. See Built for your workflow and Authentication.


Should I start on paper or live?

Start on paper. Endpoints, schemas, and error semantics match live, but you are working with simulated funds. Validate order placement, cancellation, WebSocket subscriptions, and account reads end-to-end on paper keys, then switch to live credentials when you are ready. Locates require a live account for the full quote → accept workflow - see Account Types.


Orders

Why does my order return 200 OK but orderStatus: "Rejected"?

The API returns 200 OK once the request body is structurally valid. Whether the order is accepted for routing is reflected in orderStatus. Common rejections include:

  • R78 - market order submitted outside Regular Trading Hours (9:30 AM-4:00 PM ET). Use a Limit order with timeInForce: "Day_Plus" or "GTC_Plus" for extended hours.
  • R95 - you tried to open both a long and a short on the same symbol at the same time. Cancel one direction first.
  • R114 - clientOrderId was already used in this session. Generate a fresh unique value for every order - ids are not reusable after cancellation.
  • R118 - stop price is on the wrong side of the current quote for the order direction.
  • R130 - cancel attempted on an order that is already terminal (not a placement rejection - see below).

Always read orderStatus after a 200 response. On live routed orders, the POST body may show PendingNew + text: null first; poll GET /order/{clientOrderId} for the final reason within ~50 ms. See Order rejections and Validation errors.


Why didn't I get the rejection reason on POST /order?

Many rejections on live accounts are asynchronous when you send an explicit route (for example "SMART") — R24, R54, R95, platform NBBO limits, and R118 without route follow this pattern. POST /order returns PendingNew and text: null; the reason appears on GET /order/{clientOrderId} and GET /orders moments later.

Exception — R118 with an explicit route: the server may reject synchronously with orderStatus: "Rejected", text: null, and a rewritten clientOrderId (appends INVALID + timestamp). GET /order/{yourOriginalId} returns 404. Scan GET /orders for the Rejected row; do not cancel (R130 overwrite). See Order rejections.


Why does GET /order return 404 for my rejected order?

Three common causes:

  1. Registration race — poll for 1–2 s after PendingNew POST before giving up.
  2. clientOrderId rewrite — R118 routed rejections return a different clientOrderId on POST (with an INVALID suffix). Always use post.clientOrderId from the POST response, not the ID you sent.
  3. Reject without retrievable single-order row — even the POST clientOrderId may not work on GET /order; the row exists only on GET /orders with Rejected + text: null.

Why did cancel show R130 instead of the original rejection?

If the client calls DELETE /orders/{clientOrderId} on an order that is already Rejected, the API returns HTTP 200 but overwrites text with R130: Cancel Request Rejected: …, masking the original reason (for example R118). Read and display the rejection from GET /order/{clientOrderId} before attempting cancel, and never cancel terminal orders. See Do not cancel already-rejected orders.


Where are rejection timestamps?

Every order row carries startTime (submission) and lastUpdated (last state change). On async rejections, lastUpdated is when orderStatus: "Rejected" and text landed — a few milliseconds after startTime (within ~50 ms on live routed orders).


Does the Portfolio WebSocket carry rejection events?

Yes, for async rejections (R24, R54, R95, R118 without route, platform NBBO). Subscribe to "Order" on wss://webapi.trade0.click/stream/portfolio. Rejected orders push an Order update with orderStatus: "Rejected", populated text, and timestamps. Many async rejections send two pushes — first PendingNew, then Rejected ~milliseconds later.

R118 with an explicit route may reject synchronously with text: null and may not push a useful WS update — do not rely on WebSocket alone for all rejection paths. Connect and subscribe before placing orders, and always fall back to REST (GET /order + GET /orders). See Portfolio Stream → Order rejections.


Can I modify a working order?

There is no modify endpoint. Use the cancel-then-replace pattern: DELETE /v1/api/accounts/{accountId}/orders/{clientOrderId} the original, wait for orderStatus: "Canceled", then POST /order a replacement with a new clientOrderId. Reusing the original id returns R114 even after cancellation - the id is consumed permanently the moment it is first accepted. See Modifying an order.


What's the difference between GET /orders and GET /orders/start-date?

Both are about orders, but they sit at different levels:

  • GET /orders is the today's order book - it returns every order on the account from today's session regardless of current state (rows include New, Accepted, PartiallyFilled, Filled, Canceled, Rejected, DoneForDay, …) plus any still-working orders from previous sessions (multi-day GoodTillCancel / GTC_Plus that haven't filled or expired yet). Each row carries clientOrderId directly, plus order-level fields (orderStatus, executed, priceAvg, limitPrice, timeInForce, …). Use this to manage live orders and to read terminal-state outcomes for orders submitted today.
  • GET /orders/start-date/{date} is the historical orders archive - up to one week of post-trade history. Rows are at the fill level, not the order level: each row is one execution (tradeId, qty, price, commission, totalFees, grossProceeds, netProceeds, tradeDate), and a partial-filled order produces multiple rows. There is no clientOrderId and no orderStatus on these rows. Use this for daily P&L recaps, fee/commission rollups, and reconciliation. Paper accounts have no order history available and always return { "orders": [] }.

Why does canceling an order return 404, 400, or 401?

DELETE /v1/api/accounts/{accountId}/orders/{clientOrderId} has three distinct failure shapes:

  • 401 Unauthorized (JSON) - auth headers were missing. Body: {"statusCode":"Unauthorized","message":"Token not provided","detail":null}.
  • 404 Not found (plain text) - the order was not found (wrong ID, not yet registered, wrong account, auth mismatch, or the order is already terminal such as Filled, Canceled, or Expired). Rejected orders may return HTTP 200 from cancel instead of 404, but overwrite text with R130 - do not cancel rejected orders. See Order rejections.
  • 400 with JSON body ({"statusCode":"BadRequest","message":"CancelOrderWithResponse","detail":"Unable to fetch account orders..."}) - the {accountId} in the URL path does not match the credentials you authenticated with. For TradeZero America accounts, a common trigger is sending the portal login instead of the 2TZ account number - use the account value from GET /v1/api/accounts.

In all cases, confirm the final state via GET /orders. See Cancel behavior.


Does cancel-all work when there are no open orders?

Yes. DELETE /v1/api/accounts/orders always returns {"message":"Cancel Request Submitted Successfully"} as long as the multipart body carries a valid account field - regardless of whether any orders were open.


Are enum values case-sensitive?

Yes, but enforcement differs by field. side, orderType, securityType, and timeInForce are case-sensitive - wrong case returns 400. For openClose, always send "Open" or "Close" exactly.


Can I send fractional shares?

No. orderQuantity must be a whole integer. Sending 0.5 returns 400 Bad Request with - orderQuantity: Invalid type. Expected: integer, given: number.


What is route: "<no value>" in a rejected order?

Some orders rejected during validation show route: "<no value>" because no venue was selected. Always check orderStatus === "Rejected" and read text for the reason.


Why did my order reject with R54: Unable to reach the destination Route?

The order had no usable route. On a live account, always send an explicit route from GET /routes - omitting route can leave the order with no route assigned and produce R54 during routing. The other cause is sending a route your account doesn't have (route names vary by account, and securityType matters - e.g. a single-leg Option sent through the MLEG-only PAPERM route rejects with R54).

Fix: query GET /v1/api/accounts/{accountId}/routes, pick a routeName whose securityTypes matches your order, and send it explicitly in the route field. Query /routes per account and use the returned routeName - route availability varies by subscription. See Get available routes.


Which routes are available - and what's the difference between paper and live?

Query GET /v1/api/accounts/{accountId}/routes:

  • Paper accounts return two synthetic routes - PAPER (Stock + Option) with Day, GoodTillCancel, GoodTillCrossing and PAPERM (MLEG, Day-only).
  • Live accounts return five routes - SMART (smart router for Stocks; widest order-type and TIF set), CTDL (direct-access stock route), SMARTO (smart router for single-leg Options), SMARTM (smart router for multi-leg Options), and ARCA (direct ECN route for Stocks; adds IOC and FOK TIFs).

The exact set on a given live account depends on subscription. Always query /routes and use what the API returns for your account. See Get available routes for the field-level breakdown.


Can I cancel an order immediately after placing it?

A DELETE /v1/api/accounts/{accountId}/orders/{clientOrderId} issued before the order is registered may return 404 Not found. Poll GET /orders until the order appears before canceling, or retry the cancel after a brief delay.


Accounts & Authentication

Which account ID do I use for API calls? My TradeZero America account shows two IDs.

Use the 2TZ-prefixed account number, not your login. TradeZero America (TZA) accounts have both a login (e.g. ZKA47093) and an account number shown as "Account" (e.g. 2TZ35844); only the 2TZ account number is valid for API requests. The safest approach is to call GET /v1/api/accounts and use the account value it returns for every {accountId} path parameter and request body. Sending the login instead fails account resolution (404, or the "Account for User was not found, or User doesn't have entitlements." error on locate and order calls). Most other TradeZero accounts have a single ID. See Account Information.


How do I tell paper accounts from live accounts in the API response?

Read the accountType field from GET /v1/api/account/{accountId}. Paper accounts return "Paper"; live accounts return "Live" (or another non-"Paper" value reflecting how the account is classified). Do not pattern-match account IDs - use the server-issued field. See Account Types.


Why am I getting 404 Not found instead of 401 Unauthorized when my credentials are wrong?

Most read endpoints return 404 Not found when credentials are missing or invalid. The order-cancel endpoint (DELETE /v1/api/accounts/{accountId}/orders/{clientOrderId}) returns 401 Unauthorized instead. Handle both status codes in your client. See Error handling.


Do I need to refresh or rotate my API keys?

No automatic expiry - keys stay valid until you explicitly regenerate or disable them in the portal. Rotate if you suspect a leak: use Regenerate Secret to replace just the secret (invalidating the old one immediately) or Disable + Generate for a full key replacement. Every lifecycle event is logged in the portal's Audit Trail and triggers an email. See Key lifecycle rules.


Short Locates

Why does POST /quote return 200 OK but no offer ever appears in /history?

On paper accounts, /quote returns 200 OK but the quote settles in /history as locateStatus: 56 Rejected because locates run on live accounts only. Use a funded live account for the full quote → accept → inventory workflow. See Account Types.

On live accounts, quotes that find no available inventory may produce a Rejected row with text: "No shares available." rather than an Offered row.


How long does an offered locate quote stay valid?

30 seconds from when the row first reaches locateStatus: 65 (Offered). If you don't POST /accept within that window, both rows (and the .SU sibling for Reg SHO symbols) move to status 67 (Expired) and you need to issue a fresh /quote.


What is a Reg SHO threshold security and why do I see two rows in /history?

A Reg SHO threshold security is an equity security that appears on a threshold securities list under Regulation SHO due to persistent fail-to-deliver positions meeting regulatory thresholds for five consecutive settlement days. The platform returns two priced rows for these symbols: one for the Pre-Borrow offer (your original quoteReqID) and one for the cheaper Single Use offer (quoteReqID + ".SU"). Accept either one - accepting the other auto-expires. See Short Locates.


WebSocket

Do the streams send a keepalive ping?

No. The server does not send a keepalive. Implement your own reconnection logic with exponential backoff so long-lived connections can recover automatically if they drop. See Reconnection.

Are both streams available on paper accounts?

Yes. The P&L and Portfolio streams work identically on paper and live accounts. P&L updates are driven by simulated quote ticks on your paper positions. See WebSocket API.