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

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

Query-parameter keys may appear in proxy access logs. Use distinct keys for WS traffic if this is a concern, and rotate on the same schedule as your REST keys.

Connection lifecycle

Once the WebSocket opens, the server sends a greeting:

Server → client
{
  "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

Subscribe to one or more contests
{
  "action": "subscribe",
  "channel": "liquidity",
  "contest_ids": ["ct_abc123", "ct_def456"]
}
Unsubscribe
{
  "action": "unsubscribe",
  "channel": "liquidity",
  "contest_ids": ["ct_def456"]
}
Heartbeat
{ "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.

json
{
  "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

json
{ "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 ping event.

Reconnection pattern

Browser with exponential backoff
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.