Portfolio Stream
The WebSocket API is currently in beta. Features and endpoints are subject to change.
Overview
The Portfolio stream delivers real-time updates for active orders and positions - specifically changes driven by order activity (fills, partial fills, cancellations, acknowledgements). It does not deliver price-driven P&L changes; those come from the P&L Stream.
Endpoint: wss://webapi.tradezero.com/stream/portfolio
The Portfolio stream does not send a full snapshot of existing orders or positions when you first connect. See the recommended usage pattern below to avoid missing updates.
Recommended usage pattern
Because the stream delivers only incremental changes, fetch your current state from the REST API before subscribing:
- Open the WebSocket connection and start buffering incoming messages - do this first so you don't miss any updates that arrive while the REST call is in flight.
- Fetch current orders and positions via REST:
GET /v1/api/accounts/{accountId}/orders- every working order, including multi-day GTCsGET /v1/api/accounts/{accountId}/positions- current open positions
- Apply any buffered WebSocket messages on top of the REST snapshot - this gives you a consistent starting state.
Usage
Step 1 - Connect and authenticate
Connect to the endpoint and complete the authentication handshake described in About WebSocket API. Once you receive CONNECTED, proceed to step 2.
Step 2 - Subscribe
Send a subscribe payload with the account ID and the channel types you want:
{
"accountId": "YOUR_ACCOUNT_ID",
"subscriptions": ["Order", "Position"]
}
| Value | Channel |
|---|---|
"Order" | Order state changes (accepted, filled, partially filled, canceled) |
"Position" | Position changes driven by order fills |
You can subscribe to one or both channels in a single message. To subscribe to additional accounts, send further subscribe payloads at any time - each one receives its own confirmation.
["order", "position"] (lowercase) is accepted the same as ["Order", "Position"]. Duplicate entries in the array (e.g. ["Order", "Order"]) are also accepted and treated as a single subscription. The subscriptions field must be a JSON array - sending a string returns INVALID_DATA.
Any extra fields you include in the subscribe payload beyond accountId and subscriptions are ignored by the server - only those two fields are read.
Step 3 - Receive the confirmation
The server responds with a meta message confirming the subscription:
{
"ts": 1700000016653,
"accountId": "TZP12345678",
"action": "meta",
"requestConfirmed": true
}
After this confirmation, order and position updates flow in as they occur.
Real-time updates
All update messages share the same top-level envelope:
| Field | Type | Description |
|---|---|---|
ts | number | Unix timestamp in milliseconds |
accountId | string | The account the update belongs to |
action | string | Always "update" for data messages |
subscription | string | "Order" or "Position" |
Order updates
Received whenever an order changes state.
{
"ts": 1771864795549,
"accountId": "TTE12345678",
"action": "update",
"subscription": "Order",
"order": {
"accountId": "TTE12345678",
"canceledQuantity": 0,
"clientOrderId": "my-buy-001",
"executed": 1,
"lastPrice": 398.41,
"lastQuantity": 1,
"lastUpdated": "2026-02-23T16:39:55.259+00:00",
"leavesQuantity": 0,
"limitPrice": 398.41,
"maxDisplayQuantity": 0,
"openClose": "Open",
"orderQuantity": 1,
"orderStatus": "Filled",
"orderType": "Limit",
"pegDifference": 0,
"pegOffsetType": "Price",
"priceAvg": 398.41,
"priceStop": 0,
"route": "SMART",
"securityType": "Stock",
"side": "Buy",
"startTime": "2026-02-23T16:37:05.909+00:00",
"strikePrice": 0,
"symbol": "TSLA",
"text": null,
"timeInForce": "Day",
"tradedSymbol": "TSLA"
}
}
The same shape is delivered for paper accounts, with accountId set to a TZP… value and route: "PAPER" (or "PAPERM" for multi-leg). Paper rows often carry text: "TRAFIX_SIM" instead of null.
Order field reference
| Field | Type | Description |
|---|---|---|
accountId | string | Account the order belongs to |
canceledQuantity | number | Quantity canceled |
clientOrderId | string | Client-assigned order identifier |
executed | number | Total quantity filled so far |
lastPrice | number | Price of the most recent fill |
lastQuantity | number | Quantity of the most recent fill |
lastUpdated | string | ISO 8601 timestamp of the most recent change |
leavesQuantity | number | Remaining unfilled quantity |
limitPrice | number | The order's limit price |
maxDisplayQuantity | number | Displayed quantity for reserve/iceberg orders (0 if not applicable) |
openClose | string | "Open" or "Close" |
orderQuantity | number | Total ordered quantity |
orderStatus | string | Current status - see order lifecycle |
orderType | string | "Limit", "Market", "Stop", etc. |
pegDifference | number | Peg offset amount (0 if not a pegged order) |
pegOffsetType | string | Peg reference type (e.g. "Price") |
priceAvg | number | Volume-weighted average fill price |
priceStop | number | Stop price for stop or stop-limit orders (0 otherwise) |
route | string | Routing destination - paper accounts return "PAPER" (single-leg / equity) or "PAPERM" (multi-leg). Live accounts return real venues such as "SMART", "CTDL", "SMARTO", "SMARTM", or "ARCA". |
securityType | string | "Stock", "Option", "MLEG" |
side | string | "Buy" or "Sell" |
startTime | string | ISO 8601 timestamp when the order was placed |
strikePrice | number | Strike price for single-leg options (0 for equities) |
symbol | string | Ticker symbol (underlying ticker for multi-leg orders) |
text | string | Free-text routing notes |
timeInForce | string | "Day", "GoodTillCancel", "ImmediateOrCancel", etc. |
tradedSymbol | string | null | Symbol as executed; may differ from symbol for certain option routes |
For multi-leg ("MLEG") orders, symbol is the underlying ticker (e.g. "AAPL") and securityType is "MLEG". The individual leg details are not included in the WebSocket order update; use GET /v1/api/accounts/{accountId}/orders to retrieve the full leg structure.
Position updates
Received whenever a fill creates or modifies a position.
{
"ts": 1771864795549,
"accountId": "TTE12345678",
"action": "update",
"subscription": "Position",
"position": {
"id": "7260223163955259000",
"accountId": "TTE12345678",
"createdDate": "2026-02-23T16:39:59.559+00:00",
"dayOvernight": "Day",
"priceAvg": 398.41,
"priceClose": 0,
"priceOpen": 398.41,
"priceStrike": 0,
"putCall": "None",
"securityType": "Stock",
"shares": 1,
"side": "Long",
"symbol": "TSLA",
"updatedDate": "2026-02-23T16:39:55.259+00:00"
}
}
Position field reference
| Field | Type | Description |
|---|---|---|
id | string | Unique position identifier. Note: the REST position endpoints use positionId for the same value; the WebSocket stream uses id. |
accountId | string | Account the position belongs to |
createdDate | string | ISO 8601 timestamp when the position was first opened |
dayOvernight | string | "Day" (opened today) or "Overnight" (carried from prior session) |
priceAvg | number | Average cost basis |
priceClose | number | 0 while the position is open (it is not a previous-session close). Populated only after the position is fully closed. |
priceOpen | number | Price at which the position was originally opened |
priceStrike | number | Strike price for options (0 for equities) |
putCall | string | "Put", "Call", or "None" for equities |
securityType | string | "Stock" or "Option" |
shares | number | Current position size. Negative for short positions. |
side | string | "Long" or "Short" |
symbol | string | Ticker symbol; OCC format for options |
updatedDate | string | ISO 8601 timestamp of the most recent update |
Error handling
Subscription errors are returned as system messages ("@system": true) with status: "TERMINATED" or status: "INVALID_DATA". The connection stays open after these; you can send a corrected subscribe payload without reconnecting.
| Scenario | Status | Message |
|---|---|---|
| Account ID not found or not owned | TERMINATED | "invalid account" |
accountId sent as a number (e.g. 99999) | TERMINATED | "invalid account" |
subscriptions array is empty [] | TERMINATED | "subscription types not specified" |
subscriptions field is omitted | TERMINATED | "subscription types not specified" |
subscriptions contains an unrecognized string value | INVALID_DATA | Description of the parse error |
subscriptions is a string instead of an array | INVALID_DATA | "Error converting value ... to type 'ICollection[WsPortfolioSubscptionType]'" |
{
"@system": true,
"ts": 1700000019288,
"status": "TERMINATED",
"message": "invalid account"
}
Multiple account subscriptions
Send additional subscribe payloads on the same connection to subscribe to more accounts. Each subscription receives its own meta confirmation:
{ "accountId": "ACCOUNT_A", "subscriptions": ["Order", "Position"] }
{ "accountId": "ACCOUNT_B", "subscriptions": ["Order"] }
Complete example
The pattern below implements the recommended REST-first approach: buffer WebSocket messages while fetching the REST snapshot, then apply them on top.
const accountId = 'YOUR_ACCOUNT_ID';
const apiKey = 'YOUR_TZ-API-KEY-ID';
const apiSecret = 'YOUR_TZ-API-SECRET-KEY';
const authHeaders = {
'TZ-API-KEY-ID': apiKey,
'TZ-API-SECRET-KEY': apiSecret,
};
// Local state - seeded from REST, kept in sync by WebSocket updates.
const orders = new Map<string, any>(); // keyed by clientOrderId
const positions = new Map<string, any>(); // keyed by id (WebSocket) / positionId (REST)
// Buffer messages that arrive while the REST fetch is in flight.
const messageBuffer: any[] = [];
let restReady = false;
function applyMessage(msg: any): void {
if (msg['@system']) {
switch (msg.status) {
case 'PENDING_AUTH':
ws.send(JSON.stringify({ key: apiKey, secret: apiSecret }));
break;
case 'CONNECTED':
ws.send(JSON.stringify({ accountId, subscriptions: ['Order', 'Position'] }));
break;
case 'FAILED_AUTH':
console.error('Auth failed:', msg.message);
ws.close();
break;
case 'TERMINATED':
case 'INVALID_DATA':
console.error('Subscribe error:', msg.status, msg.message);
break;
}
return;
}
if (msg.action === 'meta') {
console.log('Portfolio stream confirmed for', msg.accountId);
return;
}
if (msg.action === 'update' && msg.subscription === 'Order') {
orders.set(msg.order.clientOrderId, msg.order);
onOrderChange(msg.order);
} else if (msg.action === 'update' && msg.subscription === 'Position') {
// The stream uses `id`; REST endpoints use `positionId` for the same value.
positions.set(msg.position.id, msg.position);
onPositionChange(msg.position);
}
}
const ws = new WebSocket('wss://webapi.tradezero.com/stream/portfolio');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (!restReady) {
messageBuffer.push(msg);
} else {
applyMessage(msg);
}
};
// Fetch the REST snapshot in parallel - apply buffered messages after.
async function bootstrap(): Promise<void> {
const [ordersR, positionsR] = await Promise.all([
fetch(`https://webapi.tradezero.com/v1/api/accounts/${accountId}/orders`, { headers: authHeaders }),
fetch(`https://webapi.tradezero.com/v1/api/accounts/${accountId}/positions`, { headers: authHeaders }),
]);
if (!ordersR.ok) throw new Error(`orders bootstrap failed: ${ordersR.status}`);
if (!positionsR.ok) throw new Error(`positions bootstrap failed: ${positionsR.status}`);
const [ordersResp, positionsResp] = await Promise.all([ordersR.json(), positionsR.json()]);
// Both endpoints return wrapped envelopes: `{ orders: [...] }` and `{ positions: [...] }`.
for (const o of ordersResp.orders ?? []) orders.set(o.clientOrderId, o);
for (const p of positionsResp.positions ?? []) positions.set(p.positionId, p);
restReady = true;
for (const msg of messageBuffer) applyMessage(msg);
messageBuffer.length = 0;
}
bootstrap().catch(console.error);
id / positionId key differenceREST position endpoints return positionId. The Portfolio WebSocket stream returns the same value as id. When merging WebSocket updates on top of a REST snapshot you need to reconcile these: either re-key the REST snapshot to use id, or query the REST endpoint after the first WebSocket position update to confirm the mapping.
For an end-to-end implementation of the connect → buffer → REST snapshot → apply pattern (with reconnection logic and full message handling), see the Portfolio Order Tracker recipe.