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

# Real-time trading stream

> One read-only WebSocket with every execution, order change, position, balance and account status change across your organization's accounts.

The real-time trading stream pushes trading activity for **all** of your
organization's trading accounts over a single WebSocket, as it happens:

* every **execution**, including each partial fill of an order that is still working
* **order** changes (submitted, cancelled)
* **position** updates (net quantity, average price, open P\&L, version)
* **account** updates (balance, equity, P\&L, drawdown, status)
* **rule violations**

It is built for intraday risk monitoring: frames arrive within a fraction of a
second of the execution, and the stream is read-only, so it can never place,
change or cancel anything.

> [!NOTE]
>
> The stream is enabled per organization. Contact us to turn it on for yours.
> Until it is enabled, connections are refused with HTTP 403.

### How it compares

| | Real-time trading stream | Event stream and webhooks (`fill.created`) |
| - | - | - |
| Fill granularity | Every execution, as it happens | One event per order, when the order ends |
| Positions and balances | Pushed on every change | Not included (balance via `account.balance_changed`) |
| Delivery | Live, resume window of a few minutes | Webhooks retried and signed; `?since=` replay |
| Use it for | Live risk, dashboards, alerts | Durable records, reconciliation, back-office |

Use the trading stream for what is happening now and webhooks or REST for the
durable record. Many integrations use both.

## Connect

```
wss://api.hyperprop.com/platform/v1/organization/trading/stream
```

Authenticate with your organization API key in the `X-API-Key` header of the
WebSocket handshake. The key is not accepted in the query string.

| Query parameter | Default | Description |
| - | - | - |
| `types` | all | Comma-separated frame types to receive: `execution`, `order`, `position`, `account`, `rule_violation` |
| `marks` | `true` | `false` drops position and account updates that only re-mark open P\&L (no fill, no balance or status change) |
| `resume` | — | The `id` of the last event you processed; see [Resume after a reconnect](#resume-after-a-reconnect). The `Last-Event-ID` header works too |

If the handshake is refused, the response body is JSON with an `error`
message:

| Status | Meaning |
| - | - |
| `400` | Unknown value in `types` |
| `401` | Missing, invalid, deactivated or expired API key |
| `403` | Organization not active, or the stream is not enabled for it |
| `429` | Connection limit for the key reached, or too many connection attempts |
| `503` | Temporarily unavailable or restarting; retry with backoff |

## Frames

Every message is one JSON object with a `type`. Event frames share this
envelope:

| Field | Description |
| - | - |
| `type` | `execution`, `order`, `position`, `account` or `rule_violation` |
| `id` | Event id. Pass the last one you processed as `resume` when you reconnect |
| `seq` | Increases by one for each event of your organization in this stream session. Without filters, a jump means events were missed; with `types` or `marks=false`, jumps are expected |
| `receivedAt` | When the stream received the event (ISO 8601) |
| `account` | `{ id, accountNumber, customerId }` — `customerId` is the reference you set when creating the account |
| `data` | The payload, per type below |

### `execution`

One frame per execution. A 5-lot limit order filled 2 then 3 produces two
frames, each with its own `executionId`, `executedQuantity` and
`executedPrice`, and the order's running state after that execution.

```json theme={null}
{
  "type": "execution",
  "id": "ev_q3J8l1mZb9Xo2cWk4tYp0A",
  "seq": 1842,
  "receivedAt": "2026-10-26T13:30:00.131Z",
  "account": { "id": "9b2f4e1a-6c3d-4f8e-a1b2-3c4d5e6f7a8b", "accountNumber": "HP-104233", "customerId": "cust_8812" },
  "data": {
    "executionId": "5f0c6a52-1d4e-4b7a-9c3e-2a1b0c9d8e7f",
    "executedQuantity": 2,
    "executedPrice": 21000.25,
    "executedAt": "2026-10-26T13:30:00.124Z",
    "side": "buy",
    "fee": 0.74,
    "commission": 1.0,
    "realizedPnl": 0,
    "accountBalanceAfter": 50123.26,
    "order": {
      "orderId": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
      "clientOrderId": null,
      "contract": "MNQZ6",
      "symbol": "MNQ",
      "product": "CME",
      "side": "buy",
      "orderType": "limit",
      "status": "partially_filled",
      "quantity": 5,
      "filledQuantity": 2,
      "remainingQuantity": 3,
      "averageFillPrice": 21000.25,
      "limitPrice": 21000.25,
      "stopPrice": null,
      "bracketType": null,
      "bracketParentId": null,
      "reason": null,
      "rejectReason": null,
      "createdAt": "2026-10-26T13:29:58.002Z",
      "updatedAt": "2026-10-26T13:30:00.124Z"
    }
  }
}
```

`order.status` is `partially_filled` while the order is still working and
`filled` on its last execution. `accountBalanceAfter` is the account balance
right after this execution.

### `order`

Order changes other than executions. `data.change` is `submitted` or
`cancelled`; `data.order` has the same fields as `execution.data.order`.

```json theme={null}
{ "type": "order", "id": "ev_…", "seq": 1843, "receivedAt": "…", "account": { … },
  "data": { "change": "cancelled", "order": { "orderId": "…", "status": "cancelled", "quantity": 5, "filledQuantity": 2, "remainingQuantity": 3, … } } }
```

### `position`

The position in one contract after a change. It follows the `execution` frame
that caused it.

```json theme={null}
{ "type": "position", "id": "ev_…", "seq": 1844, "receivedAt": "…", "account": { … },
  "data": {
    "contract": "MNQZ6",
    "quantity": 2,
    "side": "long",
    "averagePrice": 21000.25,
    "unrealizedPnl": 0,
    "realizedPnl": 0,
    "version": 3317,
    "openedAt": "2026-10-26T13:30:00.124Z",
    "updatedAt": "2026-10-26T13:30:00.124Z",
    "reason": "fill"
  } }
```

| Field | Description |
| - | - |
| `quantity` | Net quantity: positive long, negative short, `0` flat |
| `side` | `long`, `short` or `flat` |
| `version` | Increases with every change to the position; ignore a frame with a lower `version` than one you already applied |
| `reason` | `fill`, `closed`, `liquidation`, `mark` (open P\&L re-marked, no fill) or `other` |

Open positions are re-marked about once a second while the market moves
(`reason: "mark"`). Pass `marks=false` if you only want changes caused by
executions.

### `account`

```json theme={null}
{ "type": "account", "id": "ev_…", "seq": 1845, "receivedAt": "…", "account": { … },
  "data": {
    "status": "active",
    "balance": 50123.26,
    "equity": 50123.26,
    "highWaterMark": 50250.0,
    "unrealizedPnl": 0,
    "totalPnl": 123.26,
    "dailyPnl": 123.26,
    "drawdown": 126.74,
    "violationReason": null
  } }
```

A status change (for example to `failed` or `passed`) arrives as an `account`
frame with the new `status`. Updates where only open P\&L moved are marks and
are dropped by `marks=false`.

### `rule_violation`

```json theme={null}
{ "type": "rule_violation", "id": "ev_…", "seq": 1846, "receivedAt": "…", "account": { … },
  "data": { "rule": "daily_loss", "reason": "Daily loss limit reached" } }
```

### Control frames

| Frame | Meaning |
| - | - |
| `connected` | First frame: `organizationId`, `streamId`, `serverTime`, `heartbeatIntervalMs`, your filters and the replay window |
| `heartbeat` | Every 15 seconds, with `lastEventId`. No frame for 45 seconds means the connection is dead: reconnect |
| `pong` | Reply to a `{"type":"ping"}` you send (optional; the server also sends WebSocket pings) |
| `resume.complete` | Replay finished: `replayed` frames were sent, live frames follow |
| `resume.unavailable` | The `resume` id is outside the replay window. Reconcile over REST, then continue live |
| `stream.gap` | Events may have been lost at this point in the stream. Reconcile over REST |
| `reconnect` | The server is restarting. Reconnect with `resume` set to `lastEventId` |

## Ordering and delivery

* Frames for one account arrive in the order they happened. Frames of
  different accounts can interleave.
* The live stream delivers each event once. After a reconnect with `resume`,
  events can be repeated: deduplicate executions by `data.executionId` and
  other frames by `id`.
* Apply positions by `version` and treat `execution` frames as the source of
  truth for executed quantity.

## Resume after a reconnect

The stream keeps a short replay window (the last 5 minutes, up to 20,000
events, per organization). Store the `id` of the last frame you processed and
reconnect with it:

```
wss://api.hyperprop.com/platform/v1/organization/trading/stream?resume=ev_q3J8l1mZb9Xo2cWk4tYp0A
```

You receive every frame after that event, then `resume.complete`, then live
frames, with no gap and no overlap between the two. If the id is outside the
window you get `resume.unavailable`: fetch current state over REST (below) and
continue from the live frames.

### Reconcile over REST

* Balance, equity and status: `GET /platform/v1/organization/trading-accounts/{accountId}`
* Executions: `GET /platform/v1/organization/trading-accounts/{accountId}/order-events?eventType=fill_partial,fill_final`
* Finished orders for the day: `GET /platform/v1/organization/snapshots/fills`

## Limits

| Limit | Value |
| - | - |
| Concurrent connections per API key | 5 |
| Connection attempts per API key | 20 per minute |
| Messages you send | 20 per 10 seconds (only `ping` is understood) |
| Unsent backlog per connection | About 4 MB |

Read frames as fast as they arrive. If a connection falls behind and its
backlog passes the limit, the stream closes it with code `4008` rather than
let it slow down anyone else. Reconnect with `resume` to catch up.

### Close codes

| Code | Meaning | What to do |
| - | - | - |
| `1012` | Server restarting | Reconnect with `resume` |
| `4000` | Missed heartbeat | Reconnect with `resume` |
| `4001` | API key revoked or expired, or the stream was disabled | Do not retry with the same key |
| `4008` | Slow consumer | Read faster (move processing off the receive loop), then reconnect with `resume` |
| `4029` | Too many messages sent | Send fewer messages; reconnect |

Reconnect with exponential backoff (for example 0.5 s, 1 s, 2 s … up to 30 s,
with jitter), except after `4001`.

## Example clients

```javascript Node.js theme={null}
// npm install ws
const WebSocket = require('ws');

const URL = 'wss://api.hyperprop.com/platform/v1/organization/trading/stream';
const API_KEY = process.env.HYPERPROP_API_KEY; // hp_live_your_key_here
let lastEventId = null; // persist this if your process restarts
let backoffMs = 500;

function connect() {
  const url = lastEventId ? `${URL}?resume=${encodeURIComponent(lastEventId)}` : URL;
  const ws = new WebSocket(url, { headers: { 'X-API-Key': API_KEY } });
  let watchdog;
  const alive = () => {
    clearTimeout(watchdog);
    watchdog = setTimeout(() => ws.terminate(), 45_000);
  };

  ws.on('open', () => { backoffMs = 500; alive(); });
  ws.on('message', (raw) => {
    alive();
    const frame = JSON.parse(raw);
    switch (frame.type) {
      case 'execution':
        console.log(frame.account.accountNumber, frame.data.order.contract,
          frame.data.side, frame.data.executedQuantity, '@', frame.data.executedPrice,
          `${frame.data.order.remainingQuantity} left`);
        break;
      case 'position':
        console.log(frame.account.accountNumber, frame.data.contract,
          'net', frame.data.quantity, 'v', frame.data.version);
        break;
      case 'resume.unavailable':
      case 'stream.gap':
        // Fetch balances and executions over REST, then carry on.
        break;
    }
    if (frame.id && frame.type !== 'stream.gap') lastEventId = frame.id;
    if (frame.type === 'reconnect' && frame.lastEventId) lastEventId = frame.lastEventId;
  });
  ws.on('close', (code) => {
    clearTimeout(watchdog);
    if (code === 4001) return console.error('API key no longer valid');
    setTimeout(connect, backoffMs + Math.random() * 250);
    backoffMs = Math.min(backoffMs * 2, 30_000);
  });
  ws.on('error', () => {}); // 'close' follows
}

connect();
```

```python Python theme={null}
# pip install websockets
import asyncio, json, os, random
import websockets

URL = "wss://api.hyperprop.com/platform/v1/organization/trading/stream"
API_KEY = os.environ["HYPERPROP_API_KEY"]  # hp_live_your_key_here

async def run():
    last_event_id, backoff = None, 0.5
    while True:
        url = f"{URL}?resume={last_event_id}" if last_event_id else URL
        try:
            async with websockets.connect(url, additional_headers={"X-API-Key": API_KEY}) as ws:
                backoff = 0.5
                while True:
                    frame = json.loads(await asyncio.wait_for(ws.recv(), timeout=45))
                    if frame["type"] == "execution":
                        d = frame["data"]
                        print(frame["account"]["accountNumber"], d["order"]["contract"],
                              d["side"], d["executedQuantity"], "@", d["executedPrice"])
                    elif frame["type"] in ("resume.unavailable", "stream.gap"):
                        pass  # reconcile over REST
                    if frame.get("id") and frame["type"] != "stream.gap":
                        last_event_id = frame["id"]
        except websockets.ConnectionClosed as closed:
            if closed.rcvd and closed.rcvd.code == 4001:
                raise SystemExit("API key no longer valid")
        except (OSError, asyncio.TimeoutError):
            pass
        await asyncio.sleep(backoff + random.random() / 4)
        backoff = min(backoff * 2, 30)

asyncio.run(run())
```

Process frames quickly and hand heavy work (database writes, risk models) to a
queue, so the receive loop always keeps up.

## Measuring latency

Each `execution` carries `executedAt` (when the execution happened) and
`receivedAt` (when the stream received it). Compare `executedAt` with your own
receive time to measure delivery to your system. Keep your server clock
synchronized (NTP) for meaningful numbers.


## Related topics

- [Real-time event stream (WebSocket)](https://docs.hyperprop.com/platform-api/events/real-time-event-stream-websocket.md)
- [Real-time WebSocket for trading events](https://docs.hyperprop.com/trade-api/websocket/real-time-websocket-for-trading-events.md)
- [Introduction](https://docs.hyperprop.com/introduction.md)
- [Get open positions with real-time P&L](https://docs.hyperprop.com/trade-api/positions/get-open-positions-with-real-time-p&l.md)
- [Get copy trading status and positions](https://docs.hyperprop.com/trade-api/copy-trading/get-copy-trading-status-and-positions.md)


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