Skip to main content

Member streams

Your app can hold one WebSocket stream per member, opened with that member's access token. On it you subscribe to the member's Tradovate order, execution, position and execution-report events, stream their account P&L, and call the Tv_ functions, just as the member could with their own key. Every rule from Delegated trading applies: the token's scopes, the accounts the member shared, your allowed environments, your partner policy and your request budgets.

The stream carries origin: "tradovate" only. NinjaTrader 8 functions, NT8 events and market data are not available to a member token.

Connect​

wss://app.crosstrade.io/ws/stream
Authorization: Bearer <member access token>

The token needs tv:read. The first frame confirms the connection and when the token expires:

{"type": "auth", "origin": "tradovate", "seq": 1, "epoch": 1761400000000, "state": "authenticated", "expiresAt": "2026-10-25T19:00:00Z"}

A refused handshake answers with the usual JSON error and HTTP status, for example 401 invalid_token, 403 partner_suspended or 403 plan_not_entitled, 429 rate_limited with Retry-After, or 503 partner_api_unavailable.

One stream per member. Opening a second stream for the same member closes the first with "Session active elsewhere", the same rule as a retail connection. Your server can hold streams for all your members at once. Each connect counts as one Partner and Management call against your budget. Failed sign-ins are limited per source address, so a misconfigured client cannot lock out the rest.

Subscribe and stream P&L​

{"action": "subscribe", "origin": "tradovate", "events": ["orders", "executions", "positions", "executionReports"]}
{"action": "streamPnl", "origin": "tradovate", "enabled": true}

Frames have the shapes described in Tradovate over the WebSocket. What reaches you depends on what the member shared:

  • When every Tradovate login the member linked is shared with your app (in environments you may use), you receive every event and P&L row, the same as the member would.
  • Otherwise you receive frames only for accounts on the shared logins. Events whose account cannot be determined are held back, resync frames list only shared accounts, and pnlUpdate lists only shared accounts. status frames arrive for your allowed environments.

A subscription with origin: "nt8" answers origin_not_delegated.

Call functions​

Send rpc messages with Tv_ names, as described in the WebSocket API. Each call is authorized like the matching Trading API request:

  • Reads need tv:read; anything that places, changes, cancels or closes needs tv:trade.
  • Every account a call names must belong to a shared login in an allowed environment (identity_not_consented, environment_not_allowed).
  • Calls with no account (Tv_ListPositions or Tv_ListOrders without account, Tv_AccountSnapshot, Tv_CashBalances, Tv_MarginSnapshots, Tv_Status, Tv_CancelAllOrders, Tv_FlattenEverything, Tv_Flatten without account) need every linked login shared. Tv_ListAccounts lists the accounts on the shared logins.
  • Tv_GetOrder and Tv_GetOrderLifecycle need the order's account.
  • Suspended partners keep reads, cancels, closes and flattens; entries answer partner_suspended.
  • Each call counts against the same budgets as its HTTP twin, including the member's own budget. A refusal arrives in data with retryAfter.

Commands need an idempotency key. Put it beside id, not inside args:

{"action": "rpc", "id": "entry-1", "api": "Tv_PlaceOrder", "idempotencyKey": "4f6c2a7e-entry-1",
"args": {"account": "DEMO12345678", "instrument": "MNQZ6", "action": "Buy", "qty": 1, "orderType": "Market",
"atmQtys": "1", "atmStops": "40", "atmTargets": "80", "notes": "breakout-7"}}
{"id": "entry-1", "data": {"success": true, "response": {"orderStrategyId": 5551}, "operationId": "op_2Jx0wq3m4Yb8Cd1E"}}

The key works exactly like the Idempotency-Key header on the Trading API: at most one dispatch per key, the same key and body replay the stored answer (with "idempotentReplayed": true), a different body under the same key is idempotency_conflict, and an unclear result is outcome_unknown with an operationId you can read at GET /v1/api/tv/operations/{operationId}. If the stream cannot confirm a command in time, the reply is outcome_unknown: send it again with the same key to get its result, never with a new key. Keys are shared with your HTTP calls for the same member, so never reuse one across transports.

Keep the token fresh​

Access tokens last one hour, and a refresh retires the previous access token at once. After every refresh, hand the stream the new token:

{"action": "auth", "token": "<new member access token>"}
{"type": "auth", "origin": "tradovate", "seq": 42, "epoch": 1761403500000, "state": "authenticated", "expiresAt": "2026-10-25T20:00:00Z"}

The token must belong to the same member and your app; anything else answers "state": "refused" and the stream keeps its current token.

If the token expires, or the member's authorization ends (revoked consent, a revoked seat, a disabled partner), the stream sends "state": "expired" or "state": "revoked" with graceSeconds. From that moment it delivers nothing and refuses calls with invalid_token. Send a valid token within the grace period to continue; otherwise the stream closes with code 4401. A revocation is noticed within a minute; calls are checked on every request.

Errors on the stream​

CodeMeaning
origin_not_delegatedOnly origin: "tradovate" is available to a member token
operation_not_delegatedThe function is not a Tv_ function
invalid_tokenThe token expired or was revoked; send a fresh one with auth
validation_failedMissing idempotencyKey on a command, a missing account, or a conflicting second spelling of account or orderId
identity_not_consented, environment_not_allowedThe call touches an account the member did not share, or an environment you may not use
rate_limited, member_rate_limited, too_many_concurrent_requestsWait retryAfter seconds
outcome_unknownCheck the operation status and the account before anything else; never resend with a new key