Connect
The account stream pushes changes on your challenge accounts as they happen, so a mirror or trade copier does not need to poll the REST API. One socket covers every account your API key can reach.
Send your API key as a Bearer token in the
Authorization header of the
WebSocket upgrade request. Read-only and read-and-trade keys both work. Live
keys (fp_live_) connect only to the live URL and test keys (fp_test_) only
to the sandbox URL. A key restricted to specific accounts streams only those
accounts. MCP sessions cannot open the stream.
A rejected upgrade returns an HTTP status with a JSON body in the same
{"error":{"code","message"}} shape as the REST API:
- TypeScript (Node)
- Python
Snapshot first, then events
Every frame is one JSON object. The server speaks first:hellowith yourstream_id, theenvironment, and the heartbeat interval.- One
snapshotper account: the account, its working orders, and its open positions. snapshot_endonce every account’s snapshot has been sent.- Events, in the order they were observed.
snapshot arrives with "reason":"added". When an account
leaves the key’s reach (for example, it is archived), you receive
{"type":"account","event":"removed"}.
A snapshot can also arrive later with "reason":"resync". The server sends
one when it cannot determine how an order or position left an account’s
working set. Replace that account’s orders and positions with the snapshot,
exactly as on connect.
Every snapshot includes latest_fill, the account’s newest fill
({"id","settled_at"}, or null before its first fill).
Orders, positions, and accounts use exactly the JSON objects the
REST API returns, so the same parsing applies to both. Every
event carries the full object, including your client_order_id.
Events
Every event also has
seq, time (when the service observed the change, in
epoch milliseconds), and account_id.
An order that is placed and finishes before the stream observes it working,
such as a market order that fills immediately, produces its fill and
filled (or rejected) events without a preceding created event. Several
changes to one working order in quick succession can arrive as a single
edited event carrying the latest state.
Fills
A fill is one execution. Itsid is unique and matches the id of the same
fill in GET /v1/fills, so streamed and fetched fills reconcile directly.
Accounts
account events carry the account object plus locked_out, which is true
while a trader lockout blocks new trading. changes lists every field that
changed, for example ["balance"] after a fill, or a list including
status when a challenge passes, fails, or closes. balance is the realized balance;
request GET /v1/accounts/{account_id} for equity and rule room.
Take-profit and stop-loss links
Attached take-profit and stop-loss orders are linked in both directions. When an entry with attached exits fills, you receive the entry’sfill and
filled events, then a created event for each exit order. Each exit names
its entry in parent_order_id and its role in bracket_role (take_profit
or stop_loss). The filled entry names them in take_profit_order_id and
stop_loss_order_id. Changing an entry’s attached exit prices before it fills
produces an edited event on the entry; changing an exit after the fill
produces an edited event on that exit.
Cancel and close reasons
Canceled orders carrycancel_reason: user, oco, reduce_only,
replaced, liquidation, expired, account_closed, trading_restricted,
venue_policy, or copy. Closed positions carry close_reason: user,
take_profit, stop_loss, liquidation, reversal, or system. See
Trading with the API
for each value. Treat an unknown value as a generic cancellation or close.
Sequence numbers and resume
seq increases by one on every snapshot and event frame of a stream, starting
at 1. hello, heartbeat, pong, notice, and error frames have no
seq. Store the last seq you processed and the time of the last frame
you processed that has one (heartbeats included).
To recover after a disconnect, reconnect with all three:
- If the server still holds your stream (for about 60 seconds after the
disconnect),
helloreports"resumed":trueand you receive every frame afterlast_seq, with no new snapshot. - Otherwise, for example after a service restart or deploy,
helloreports"resumed":falsewith a newstream_id. Each account’s snapshot follows, and right after it the fills, filled, canceled, rejected, and expired orders, and closed positions sincesince, each flagged"replay":true. Replace your local state with the snapshot; use the replayed events to record what happened while you were away. Replayed events can repeat ones you already processed just before the disconnect: deduplicate byid.
snapshot_end then reports replay.complete. Replay reaches back about 100
seconds. If complete is false (you were away longer, or
incomplete_accounts lists an account), or you connected without since,
compare each snapshot’s latest_fill.id with the last fill you processed for
that account. If they differ, request GET /v1/fills for the account (newest
first) and page until you reach your last processed fill. An order you held as
working that is missing from the snapshot and from the replay ended while you
were away: GET /v1/orders/{order_id} returns its outcome.
Keepalive and closing
The server sends{"type":"heartbeat"} every 15 seconds and a WebSocket ping
every 20 seconds. A heartbeat with "upstream_connected":false means the
service is reconnecting to its data source and events may be delayed; they
resume automatically, and nothing that finished in the meantime is skipped.
A socket that stops answering for 60 seconds is closed. To measure latency,
send {"op":"ping","id":1}; the server replies with
{"type":"pong","id":1,"time":...}. No other client messages are needed.
Watch for silence on your side too: if no frame of any kind arrives for 45
seconds, treat the connection as dead even if your WebSocket library still
reports it open. Close it and reconnect with resume, last_seq, and since
as described above. Network paths can drop a connection without delivering a
close, and only this check catches that.
An
error frame with a code and message precedes 4001, 4013, and
1011 closes. A notice frame with code accounts_truncated means the key
reaches more accounts than one stream follows; create a key with an account
allowlist instead.
Limits
- Open one socket per key: it already covers all of the key’s accounts. A key may hold up to 8 sockets.
- One address may hold about a thousand sockets, which suits a service that streams many customers’ keys from one server. Reconnect with jitter after a restart instead of all at once.
- The stream reports changes; it does not accept orders. Place and manage orders through the REST API.