# Account streaming Source: https://docs.myfundedperpetuals.com/account-streaming Receive every order, fill, position, and balance change on your accounts over one authenticated WebSocket. ## 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. | Environment | URL | | - | - | | Live | `wss://account-stream.myfundedperpetuals.com/v1/stream` | | Sandbox | `wss://account-stream.myfundedperpetuals.com/v1/sandbox/stream` | 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: | Status | Meaning | | - | - | | `401` | Missing, invalid, revoked, or expired credential, or a key for the other environment. | | `403` | `sandbox_access_expired`: sandbox access requires a challenge purchase in the last 90 days. | | `429` | Too many sockets from your address or for this key. Wait for `Retry-After` and back off. | | `503` | The service is restarting or at capacity. Retry after `Retry-After` with jitter. | > [tab] TypeScript (Node) > > ```ts theme={null} > import WebSocket from "ws"; > > const socket = new WebSocket( > "wss://account-stream.myfundedperpetuals.com/v1/stream", > { headers: { Authorization: `Bearer ${process.env.FP_API_KEY}` } }, > ); > socket.on("message", (data) => { > const frame = JSON.parse(String(data)); > if (frame.type === "heartbeat") return; > console.log(frame.seq, frame.type, frame.event ?? "", frame.account_id ?? ""); > }); > socket.on("close", (code, reason) => > console.log("closed", code, String(reason)), > ); > ``` > [tab] Python > > ```python theme={null} > import asyncio, json, os > import websockets > > async def main(): > async with websockets.connect( > "wss://account-stream.myfundedperpetuals.com/v1/stream", > additional_headers={"Authorization": f"Bearer {os.environ['FP_API_KEY']}"}, > ) as socket: > async for message in socket: > frame = json.loads(message) > if frame["type"] != "heartbeat": > print(frame.get("seq"), frame["type"], frame.get("event"), frame.get("account_id")) > > asyncio.run(main()) > ``` ## 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](/api-reference) returns, so the same parsing applies to both. Every event carries the full object, including your `client_order_id`. ## Events | `type` | `event` | Payload | When | | - | - | - | - | | `order` | `created` | `order` | A new working order (resting or executing). | | `order` | `edited` | `order` | A working order changed: price, size, attached TP/SL, or its state. | | `order` | `triggered` | `order` | A stop or take order's trigger fired and it began executing. | | `order` | `partially_filled` | `order` | Part of the order filled. Limit orders normally fill in full. | | `order` | `filled` | `order` | The order filled. Its `fill` event comes first. | | `order` | `canceled` | `order` | The order was canceled. See `cancel_reason`. | | `order` | `rejected` | `order` | The order was rejected. See `reject_reason` and `reject_rule_id`. | | `order` | `expired` | `order` | A GTD order reached its expiry time, or an order could not execute. | | `fill` | none | `fill` | One execution. See the fill fields below. | | `position` | `opened` | `position` | A new position. | | `position` | `changed` | `position` | Size, entry price, margin, or other position fields changed. | | `position` | `closed` | `position` | The position closed. See `close_reason`. | | `account` | `updated` | `account` | Balance, status, or lockout changed. `changes` lists the changed fields. | | `account` | `removed` | none | The account is no longer reachable with this key. | | `price_alert` | `triggered` | `alert` | One of your price alerts fired. See [Price alerts](#price-alerts). | Every event also has `seq` and `time` (when the service observed the change, in epoch milliseconds). Account, order, fill, and position events also have `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. | Field | Meaning | | - | - | | `id` | Unique fill identifier. | | `order_id` | The order this fill belongs to. | | `client_order_id` | Your order identifier, when you supplied one. | | `price`, `size` | Execution price and size in base-asset units. | | `notional`, `fee` | Fill value and trading fee in USD. | | `realized_pnl` | Realized profit or loss on the reduced portion, before fees. | | `time` | Execution time in epoch milliseconds. | | `position_side_after` | `long`, `short`, or `flat`: the position in this market after the fill. | | `position_size_after` | The position size in base-asset units after the fill. `0` when flat. | | `side`, `order_type` | `buy` or `sell`, and the order type that executed. | | `market_id`, `symbol` | The market, as returned by `/v1/markets`. | ### 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. ### Price alerts A `price_alert` event fires once each time one of your [price alerts](/trading-with-api#price-alerts) triggers, including each fire of a re-armed alert. Alerts belong to your user, so every key you own receives them, including keys limited to certain accounts. | Field | Meaning | | - | - | | `fire_id` | Unique per fire. Use it to deduplicate. | | `alert_id` | The alert's `id`, as in `GET /v1/price-alerts/{id}`. | | `alert` | The alert as it was when it fired: `id`, `market_id`, `provider`, `symbol`, `coin`, `price`, `condition`, `note`, `price_source`, `trigger_price`, `triggered_at`, and `status` `triggered`. | The event is sent even if the alert was re-armed, edited, or deleted right after it fired. Price alerts are not part of the snapshot. A stream receives the fires from when it connects, and can include one from up to 10 seconds earlier, so deduplicate by `fire_id`. On a fresh connection with `since`, fires after `since` are replayed after `snapshot_end`. For a longer gap, list `GET /v1/price-alerts`: triggered alerts show `triggered_at`, but an alert re-armed since it fired shows only its current state. ## 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'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](/trading-with-api#reconcile-conditional-orders-and-scheduled-history) 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: ```text theme={null} wss://account-stream.myfundedperpetuals.com/v1/stream?resume=&last_seq=&since=