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

# Real-time updates

> Subscribe to Server-Sent Events for live market data, orders, positions, and trades

Two Server-Sent Event streams deliver live data: one for your account and trading activity, one for the raw order book.

<Info>
  **Prerequisites:** an active VOOI API token and at least one connected exchange. See [API Tokens](/docs/tokens) and [Use your own keys](/docs/exchanges) if you don't have them yet.
</Info>

<Note>
  The streaming endpoints are under active development. Event shapes and delivery guarantees can change without notice — pin your client to a known-good API version and re-test after upgrades.
</Note>

## Account and trading stream

Subscribing is a two-step flow: mint a one-time token, then open the SSE connection with it. The token travels in the query string so clients that cannot set request headers on a streaming GET can still authenticate.

1. [`POST /exchange/updates-token`](/docs/api-reference/exchange/create-one-time-token-update-token) — returns `{ token, expiresAt }`.
2. [`GET /exchange/updates`](/docs/api-reference/exchange/subscribe-to-real-time-updates-sse) — open with `?token=<one-time-token>`.

<CodeGroup>
  ```bash curl theme={null}
  # Step 1: mint a one-time token
  TOKEN=$(curl -s -X POST https://perps-api.vooi.io/exchange/updates-token \
    -H "Authorization: Bearer $API_TOKEN" | jq -r .token)

  # Step 2: open the SSE stream
  curl -N "https://perps-api.vooi.io/exchange/updates?token=$TOKEN"
  ```

  ```js JavaScript theme={null}
  const { token } = await fetch("https://perps-api.vooi.io/exchange/updates-token", {
    method: "POST",
    headers: { Authorization: `Bearer ${apiToken}` },
  }).then((r) => r.json());

  const es = new EventSource(`https://perps-api.vooi.io/exchange/updates?token=${token}`);
  es.addEventListener("accounts", (e) => merge(JSON.parse(e.data)));
  ```

  ```python Python theme={null}
  import httpx

  with httpx.Client() as client:
      token = client.post(
          "https://perps-api.vooi.io/exchange/updates-token",
          headers={"Authorization": f"Bearer {api_token}"},
      ).json()["token"]

      with client.stream(
          "GET",
          "https://perps-api.vooi.io/exchange/updates",
          params={"token": token},
          timeout=None,
      ) as resp:
          for line in resp.iter_lines():
              print(line)
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("POST", "https://perps-api.vooi.io/exchange/updates-token", nil)
  req.Header.Set("Authorization", "Bearer "+apiToken)
  resp, _ := http.DefaultClient.Do(req)
  var out struct{ Token string }
  json.NewDecoder(resp.Body).Decode(&out)
  resp.Body.Close()

  stream, _ := http.Get("https://perps-api.vooi.io/exchange/updates?token=" + out.Token)
  defer stream.Body.Close()
  scanner := bufio.NewScanner(stream.Body)
  for scanner.Scan() {
      // parse event: / data: lines
      fmt.Println(scanner.Text())
  }
  ```
</CodeGroup>

### Frame format

Events are emitted as named SSE frames with an auto-incrementing `id:` per connection. Each frame carries one JSON value on a single `data:` line:

```
event: accounts
id: 42
data: [{"type":"perps","token":"USDC","availableMargin":"1024.50","marginInUse":"125.00","totalBalance":"1150.00","withdrawable":"1024.50","exchange":"hyperliquid"}]

```

* `id:` is a per-connection counter that starts at `1` on every reconnect. It is **not** a resumable cursor — the server does not honor `Last-Event-ID` and does not replay missed events. Use it for local logging/ordering only.
* The server does **not** send SSE keep-alive comments (`: ping`). TCP keep-alive is enabled, but if you need an application-level idle timeout, implement it client-side.
* Each event is one JSON value on one `data:` line — never multi-line.

### Event catalog

| Event | When it fires | Payload |
| - | - | - |
| `state` | Once per exchange on subscribe — only when recent state is available for that `(user, exchange)` | Full snapshot object: `{ accounts, openOrders, positions, exchange }` |
| `accounts` | Balance change on a connected exchange | Array of account rows — same shape as [`GET /exchange/accounts`](/docs/api-reference/exchange/get-account-balances) plus `exchange` |
| `order` | Order created, updated, filled, or cancelled | Array of order rows — same shape as [`GET /exchange/orders`](/docs/api-reference/exchange/get-order-history) plus `exchange` |
| `position` | Position opened, updated, or closed | Array of position rows — same shape as [`GET /exchange/positions`](/docs/api-reference/exchange/get-open-positions) plus `exchange` |
| `trade` | Your own fill | Array of trade rows — same shape as [`GET /exchange/trades`](/docs/api-reference/exchange/get-trade-history) plus `exchange`. Own fills only — not public tape |
| `marketPrice` | Upstream tick (one frame per exchange per tick) | Array of `{ marketId, price, fundingRate, nextFundingTime, updatedAt, exchange }` for every symbol the exchange pushed |
| `restarting` | Server is reconnecting to the upstream exchange | `{ exchange }` — keep the connection open; more events follow once the link to the exchange recovers |
| `error` | The exchange stream failed (e.g. credentials revoked) | `{ error, exchange }` — the per-exchange stream is dead; reconnect with a fresh token to recover |

Example frames:

<CodeGroup>
  ```text state theme={null}
  event: state
  id: 1
  data: {"accounts":[{"availableMargin":"14.11803","marginInUse":"12.756726","totalBalance":"26.874756","type":"perps","token":"USDC","withdrawable":"14.11803","exchange":"hyperliquid"}],"exchange":"hyperliquid","openOrders":[],"positions":[{"baseSymbol":"ETH","data":null,"entryPrice":"2300.0","fundingFee":"0.04775","isolatedMargin":null,"liquidationPrice":"1924.1096078881","marginMode":"cross","marketId":"1","positionSide":"both","quoteSymbol":"USDC","realizedPnl":null,"roe":"0.3865217391","side":"buy","size":"0.0534","unrealizedPnl":"4.74726","updatedAt":null,"leverage":"10","exchange":"hyperliquid"}]}

  ```

  ```text accounts theme={null}
  event: accounts
  id: 42
  data: [{"type":"perps","token":"USDC","availableMargin":"1024.50","marginInUse":"125.00","totalBalance":"1150.00","withdrawable":"1024.50","exchange":"hyperliquid"}]

  ```

  ```text order theme={null}
  event: order
  id: 43
  data: [{"orderId":"0x1234","clientOrderId":"my-ref-1","baseSymbol":"BTC","quoteSymbol":"USD","side":"buy","positionSide":"long","size":"0.01","price":"65000","type":"limit","status":"executed","timeInForce":"gtc","triggerPrice":null,"createdAt":"2026-04-22T10:30:00Z","updatedAt":"2026-04-22T10:30:02Z","data":{},"exchange":"hyperliquid"}]

  ```

  ```text position theme={null}
  event: position
  id: 44
  data: [{"marketId":"BTC","baseSymbol":"BTC","quoteSymbol":"USD","side":"buy","positionSide":"long","size":"0.05","entryPrice":"65000","marginMode":"cross","leverage":"10","liquidationPrice":"58500","unrealizedPnl":"12.50","realizedPnl":"0","isolatedMargin":null,"roe":"0.025","fundingFee":"-0.12","updatedAt":"2026-04-22T10:30:00Z","data":null,"exchange":"hyperliquid"}]

  ```

  ```text trade theme={null}
  event: trade
  id: 45
  data: [{"tradeId":"t-9876","orderId":"0x1234","baseSymbol":"BTC","quoteSymbol":"USD","side":"buy","positionSide":"long","size":"0.01","price":"65000","fee":"0.26","feeToken":"USDC","realizedPnl":"0","createdAt":"2026-04-22T10:30:02Z","type":"trade","data":null,"exchange":"hyperliquid"}]

  ```

  ```text marketPrice theme={null}
  event: marketPrice
  id: 276
  data: [{"fundingRate":"-0.0000102131","marketId":"0","nextFundingTime":"2026-04-22T11:00:00.000Z","price":"77983.0","updatedAt":"2026-04-22T10:12:39.482Z","exchange":"hyperliquid"},{"fundingRate":"0.0000060809","marketId":"1","nextFundingTime":"2026-04-22T11:00:00.000Z","price":"2388.9","updatedAt":"2026-04-22T10:12:39.482Z","exchange":"hyperliquid"}]

  ```

  ```text restarting theme={null}
  event: restarting
  id: 7
  data: {"exchange":"hyperliquid"}

  ```

  ```text error theme={null}
  event: error
  id: 8
  data: {"error":"Upstream credentials rejected","exchange":"hyperliquid"}

  ```
</CodeGroup>

<Note>
  Exact field lists live in the API reference pages linked in the table above. SSE frames carry the same shapes, with `exchange` added to every row.
</Note>

### Snapshot vs delta

* `state` is a full snapshot for one exchange. It fires once per exchange on subscribe, and only when recent state is available for that `(user, exchange)` pair — typically after a REST read or a recent prior stream. Expect one `state` frame per connected exchange that has recent state, or none at all on a first-time connection. It is **not guaranteed**.
* `accounts`, `order`, `position`, and `trade` frames are **partial** — they contain only the rows that changed since the last frame, not a full replacement. Merge them into your client cache by identity.
* Position closures arrive as a `position` entry with `size: "0"` — the closed row is pushed, not silently dropped. Cancelled or fully-filled orders arrive with their terminal `status` (`canceled` or `executed`).
* `marketPrice` has no snapshot. The first frame for a symbol arrives with the next upstream tick. Symbol set and cadence follow the venue's own feed.

### Identity keys for merging

Use these to dedup and merge events into local state:

| Event | Key |
| - | - |
| `accounts` | `(exchange, type, token)` |
| `order` | `(exchange, orderId)` |
| `position` | `(exchange, marketId, positionSide)` |
| `trade` | `(exchange, tradeId)` |
| `marketPrice` | `(exchange, marketId)` |

### Filter by venue

The optional `exchanges` query parameter narrows the stream to specific venues. Repeat the parameter for each venue:

```
GET /exchange/updates?token=...&exchanges=hyperliquid&exchanges=aster
```

<Note>
  Comma-separated values (`?exchanges=hyperliquid,aster`) are **not** accepted — the server validates each value against the venue enum, so a CSV fails. Use repeated parameters.
</Note>

Omit the parameter to subscribe to all venues (Aster, Hyperliquid, Lighter, Robinhood, Extended, Binance, Bybit, MEXC, Ondo). Venues you have not connected stay silent — you'll still receive `marketPrice` for them (it's public), but no `accounts`, `order`, `position`, or `trade` events.

### Token lifecycle

* TTL is short — currently 2 minutes — and the exact deadline is returned as `expiresAt` on the mint response. Treat `expiresAt` as the source of truth and open the connection well before it.
* Each token is single-use. It is consumed on the first connect attempt — even one that fails. You cannot reuse a token for reconnects.
* Mint a fresh token for every reconnect. There is no documented rate limit on token minting today.
* Multiple concurrent streams per user are allowed.
* The token only gates the connection handshake. Once the stream is open it runs independently; token expiry mid-stream does not close the connection.

### Errors and reconnection

* `restarting` is transient — the server is re-establishing its link to the upstream exchange. Keep the connection open; fresh `state`/delta frames follow once it recovers.
* `error` is terminal for that exchange. The per-exchange stream will not deliver further events. To resume, close the connection, mint a new token, and reconnect.
* Network drops and HTTP-level errors surface to your SSE client's error handler. Reconnect with exponential back-off; every reconnect needs a new token.
* Connections are not closed on a schedule — they only end on network errors, client disconnect, or an `error` event from the upstream exchange.

### Recommended client flow

<Steps>
  <Step title="Seed from REST">
    Fetch initial state once per session: [`GET /exchange/accounts`](/docs/api-reference/exchange/get-account-balances), [`GET /exchange/positions`](/docs/api-reference/exchange/get-open-positions), [`GET /exchange/open-orders`](/docs/api-reference/exchange/get-open-orders). The `state` SSE event is opportunistic, so don't rely on it to seed a fresh client.
  </Step>

  <Step title="Mint a one-time token">
    [`POST /exchange/updates-token`](/docs/api-reference/exchange/create-one-time-token-update-token) — Bearer-authenticated. Returns `{ token, expiresAt }`.
  </Step>

  <Step title="Open the SSE connection">
    `GET /exchange/updates?token=<token>`. Dispatch on the `event:` name of each frame.
  </Step>

  <Step title="Merge deltas by identity">
    On each event, apply the payload rows to your cache using the identity keys above.
  </Step>

  <Step title="Reconnect on drop">
    On a transport-level error or a server-sent `error` frame, close the connection, mint a fresh token, and start over from step 2. Back off exponentially on repeated failures.
  </Step>
</Steps>

## Order book stream

[`GET /exchange/updates-orderbook`](/docs/api-reference/exchange/subscribe-to-order-book-updates-via-sse) — public order-book stream for a single asset, across one or more venues. No authentication required for the public venues.

Two event types:

* `orderBookSnapshot` — full book. Sent immediately on connect, then refreshed every 10 seconds.
* `orderBookDiff` — only changed price levels since the previous snapshot/diff. A level that has been removed appears with `size: "0"`.

This stream uses the same SSE frame format described above (`event:`/`id:`/`data:`); the `id:` line is omitted from the examples below for brevity.

```text orderBookSnapshot theme={null}
event: orderBookSnapshot
data: {"marketId":"BTC","bids":[["64990","1.25"]],"asks":[["65010","0.80"]],"date":"2026-04-22T10:30:00Z","exchange":"hyperliquid"}

event: orderBookDiff
data: {"marketId":"BTC","bids":[["64990","0"]],"asks":[["65010","1.10"]],"date":"2026-04-22T10:30:01Z","exchange":"hyperliquid"}
```

Query parameters:

* `asset` (required) — a market identifier, e.g. `BTC` or `BTCUSDC`. See [Markets](/docs/trading-markets#the-asset-query-parameter) for the accepted forms and disambiguation rules.
* `exchanges` — same repeated-parameter syntax as the account stream. Defaults to all venues.
* `levels` — cap the number of price levels per side. Even without it, a snapshot contains at most 1000 levels per side on Aster, Lighter, Robinhood, Extended, Binance, Bybit, MEXC, and Ondo.
* `significantFigures` — round prices to 2, 3, 4, or 5 significant figures (useful for bucketed depth charts).

## Next steps

<CardGroup cols={2}>
  <Card title="Orders" icon="list" href="/docs/trading-orders">
    Place orders that will emit `order` and `trade` events
  </Card>

  <Card title="Positions" icon="chart-line" href="/docs/trading-positions">
    Polling alternative for position state
  </Card>
</CardGroup>


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