Streaming (WebSocket)
Subscribe to real-time liquidity changes over a single persistent WebSocket. Lower latency than polling; the server only sends you updates for contests you've subscribed to.
Endpoint
wss://api.openmarkets.ai/flow/v1/stream?api_key=om_data_live_...The key is passed as api_key because browsers don't reliably allow custom headers on WebSocket handshakes. In server-side code, either form works, but the query parameter keeps you compatible everywhere.
Connection lifecycle
Once the WebSocket opens, the server sends a greeting:
{
"type": "connected",
"organization_id": "org_...",
"api_key_id": "key_...",
"server_time": "2026-04-17T22:00:00Z"
}You then send a subscribe message for each channel+filter combination you want. The connection stays open indefinitely; re-subscribing replaces the filter list.
Client messages
{
"action": "subscribe",
"channel": "liquidity",
"contest_ids": ["ct_abc123", "ct_def456"]
}{
"action": "unsubscribe",
"channel": "liquidity",
"contest_ids": ["ct_def456"]
}{ "action": "ping" }A connection with no contest_ids subscribed receives nothing; subscribing an empty array is effectively a no-op. The maximum number of subscribed contests per connection is 200.
Server messages
After subscribe, the server sends a confirmation, then streams liquidity.update events whenever any partner's price or size changes for a subscribed contest.
{
"type": "subscribed",
"channel": "liquidity",
"contest_ids": ["ct_abc123"]
}
{
"type": "liquidity.update",
"contest_id": "ct_abc123",
"changed_entries": [
{
"liquidity_hash": "kalshi:ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",
"partner_id": "kalshi",
"price": 0.49,
"available": 5400
}
],
"refresh_required": false,
"timestamp": "2026-04-17T22:00:01.234Z"
}
{ "type": "pong", "server_time": "..." }When the server can't fit all changes into a single event (e.g. a partner just reconnected and re-sent the full book), you'll receive refresh_required: true with an empty changed_entries. In that case, re-fetch the full liquidity via REST.
Errors
{ "type": "error", "code": "invalid_channel", "message": "..." }Possible codes: invalid_message (non-JSON), invalid_channel. Auth errors happen at upgrade time as a 401 close, not as an in-band message.
Limits
- Concurrent connections — 3 per API key (default; configurable).
- Contests per connection — 200.
- Heartbeat — the server pings every 25 seconds. If your client doesn't pong back within the next tick it will be terminated. Browser WebSocket APIs respond to protocol-level pings automatically; Node clients may need to handle the
pingevent.
Reconnection pattern
function connect() {
const ws = new WebSocket(
`wss://api.openmarkets.ai/flow/v1/stream?api_key=${KEY}`
);
let retry = 0;
ws.onopen = () => {
retry = 0;
ws.send(JSON.stringify({
action: 'subscribe',
channel: 'liquidity',
contest_ids: activeContestIds,
}));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
handle(msg);
};
ws.onclose = () => {
const delay = Math.min(30_000, 1000 * 2 ** retry++);
setTimeout(connect, delay);
};
}Not yet supported
- Orderbook depth streaming (subscribe by
position_hash). - Opportunity / arbitrage event streams.
- Settlement notifications.
These are on the roadmap and will be added without breaking existing subscriptions.