> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundedperpetuals.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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. |

<Tabs>
  <Tab title="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>

  <Tab title="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())
    ```
  </Tab>
</Tabs>

## 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. |

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.

| 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.

## 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=<stream_id>&last_seq=<seq>&since=<time>
```

* 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.

| Close code | Meaning and action |
| - | - |
| `1012` | The service is restarting. It first sends `{"type":"reconnect"}`. Reconnect now. |
| `1011` | The snapshot could not be assembled in time. Reconnect with backoff. |
| `4001` | The key was revoked, its session expired, or sandbox access lapsed. Do not retry the key. |
| `4008` | Your client read too slowly and too many frames queued. Reconnect for a fresh snapshot. |
| `4013` | The service is at capacity. Reconnect with exponential backoff and jitter. |

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](/trading-with-api).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.