Skip to main content

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:

Snapshot first, then events

Every frame is one JSON object. The server speaks first:
  1. hello with your stream_id, the environment, and the heartbeat interval.
  2. One snapshot per account: the account, its working orders, and its open positions.
  3. snapshot_end once every account’s snapshot has been sent.
  4. Events, in the order they were observed.
Treat each snapshot as the complete current state of that account. Events for an account are never sent before its snapshot. When the key gains access to a new account (for example, a new challenge purchase on a key without an account allowlist), its 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. Its id 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. Attached take-profit and stop-loss orders are linked in both directions. When an entry with attached exits fills, you receive the entry’s fill 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 carry cancel_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), hello reports "resumed":true and you receive every frame after last_seq, with no new snapshot.
  • Otherwise, for example after a service restart or deploy, hello reports "resumed":false with a new stream_id. Each account’s snapshot follows, and right after it the fills, filled, canceled, rejected, and expired orders, and closed positions since since, 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 by id.
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.