Prerequisites: an active VOOI API token and at least one connected exchange. See API Tokens and Use your own keys if you don’t have them yet.
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.
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.POST /exchange/updates-token— returns{ token, expiresAt }.GET /exchange/updates— open with?token=<one-time-token>.
Frame format
Events are emitted as named SSE frames with an auto-incrementingid: per connection. Each frame carries one JSON value on a single data: line:
id:is a per-connection counter that starts at1on every reconnect. It is not a resumable cursor — the server does not honorLast-Event-IDand 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
Example frames:
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.Snapshot vs delta
stateis 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 onestateframe per connected exchange that has recent state, or none at all on a first-time connection. It is not guaranteed.accounts,order,position, andtradeframes 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
positionentry withsize: "0"— the closed row is pushed, not silently dropped. Cancelled or fully-filled orders arrive with their terminalstatus(canceledorexecuted). marketPricehas 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:Filter by venue
The optionalexchanges query parameter narrows the stream to specific venues. Repeat the parameter for each venue:
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.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
expiresAton the mint response. TreatexpiresAtas 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
restartingis transient — the server is re-establishing its link to the upstream exchange. Keep the connection open; freshstate/delta frames follow once it recovers.erroris 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
errorevent from the upstream exchange.
Recommended client flow
1
Seed from REST
Fetch initial state once per session:
GET /exchange/accounts, GET /exchange/positions, GET /exchange/open-orders. The state SSE event is opportunistic, so don’t rely on it to seed a fresh client.2
Mint a one-time token
POST /exchange/updates-token — Bearer-authenticated. Returns { token, expiresAt }.3
Open the SSE connection
GET /exchange/updates?token=<token>. Dispatch on the event: name of each frame.4
Merge deltas by identity
On each event, apply the payload rows to your cache using the identity keys above.
5
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.Order book stream
GET /exchange/updates-orderbook — 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 withsize: "0".
event:/id:/data:); the id: line is omitted from the examples below for brevity.
orderBookSnapshot
asset(required) — a market identifier, e.g.BTCorBTCUSDC. See Markets 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
Orders
Place orders that will emit
order and trade eventsPositions
Polling alternative for position state