Liquidity.io

Realtime Data

SSE and WebSocket protocols for live market data on Liquidity.io

Liquidity.io provides realtime market data through Server-Sent Events (SSE) using the hanzo/base protocol, with WebSocket support for legacy compatibility.

SSE Connection (Primary)

The primary realtime channel uses SSE via the /api/realtime endpoint. SSE is simpler than WebSocket, works through proxies and load balancers, and automatically reconnects on failure.

Connect

GET /api/realtime
Authorization: Bearer {iam_access_token}
Accept: text/event-stream

Base URL: https://api.{env}.satschel.com/api/realtime

Protocol

The SSE stream uses the hanzo/base event protocol. Each event has a type field and a JSON data payload:

event: quote
data: {"assetId":"AAPL","last":249.93,"bid":249.87,"ask":250.01,"volume":979956,"timestamp":"2026-03-19T00:34:44Z"}

event: book
data: {"assetId":"AAPL","bids":[{"price":249.87,"size":100}],"asks":[{"price":250.01,"size":100}],"timestamp":"2026-03-19T00:34:45Z"}

event: trade
data: {"assetId":"AAPL","price":249.93,"size":50,"side":"buy","timestamp":"2026-03-19T00:34:44Z"}

Subscribe

After connecting, send subscription messages by appending query parameters:

GET /api/realtime?subscribe=quote:AAPL,book:AAPL,trade:BTC

Or subscribe to all updates for an asset:

GET /api/realtime?subscribe=all:AAPL

TypeScript Client

const BASE_URL = 'https://api.stage.satschel.com';

function connectRealtime(token: string, assets: string[]) {
  const subscriptions = assets.map(a => `all:${a}`).join(',');
  const url = `${BASE_URL}/api/realtime?subscribe=${subscriptions}`;

  const eventSource = new EventSource(url, {
    headers: {
      'Authorization': `Bearer ${token}`,
    },
  });

  eventSource.addEventListener('quote', (event) => {
    const quote = JSON.parse(event.data);
    console.log(`${quote.assetId}: $${quote.last} (${quote.changePercent}%)`);
  });

  eventSource.addEventListener('book', (event) => {
    const book = JSON.parse(event.data);
    console.log(`${book.assetId} book: ${book.bids.length} bids, ${book.asks.length} asks`);
  });

  eventSource.addEventListener('trade', (event) => {
    const trade = JSON.parse(event.data);
    console.log(`${trade.assetId} trade: ${trade.size} @ $${trade.price}`);
  });

  eventSource.addEventListener('order', (event) => {
    const order = JSON.parse(event.data);
    console.log(`Order ${order.orderId} status: ${order.status}`);
  });

  eventSource.onerror = (error) => {
    console.error('SSE connection error, reconnecting...');
    // EventSource automatically reconnects
  };

  return eventSource;
}

// Usage
const es = connectRealtime(accessToken, ['AAPL', 'BTC', 'ETH']);

// Cleanup
es.close();

React Hook

import { useEffect, useState, useRef } from 'react';

interface Quote {
  assetId: string;
  last: number;
  bid: number;
  ask: number;
  volume: number;
  change: number;
  changePercent: number;
  timestamp: string;
}

function useRealtimeQuote(assetId: string, token: string): Quote | null {
  const [quote, setQuote] = useState<Quote | null>(null);
  const esRef = useRef<EventSource | null>(null);

  useEffect(() => {
    const url = `${BASE_URL}/api/realtime?subscribe=quote:${assetId}`;
    const es = new EventSource(url);

    es.addEventListener('quote', (event) => {
      const data = JSON.parse(event.data);
      if (data.assetId === assetId) {
        setQuote(data);
      }
    });

    esRef.current = es;

    return () => {
      es.close();
    };
  }, [assetId, token]);

  return quote;
}

// Usage in component
function PriceTicker({ assetId }: { assetId: string }) {
  const quote = useRealtimeQuote(assetId, accessToken);

  if (!quote) return <div>Loading...</div>;

  return (
    <div>
      <span>{quote.assetId}</span>
      <span>${quote.last.toFixed(2)}</span>
      <span className={quote.changePercent >= 0 ? 'green' : 'red'}>
        {quote.changePercent.toFixed(2)}%
      </span>
    </div>
  );
}

Event Types

quote

Emitted when the price of a subscribed asset changes.

{
  "assetId": "AAPL",
  "last": 249.93,
  "bid": 249.87,
  "ask": 250.01,
  "open": 252.61,
  "high": 254.91,
  "low": 249.01,
  "prevClose": 254.23,
  "change": -4.30,
  "changePercent": -1.69,
  "volume": 979956,
  "timestamp": "2026-03-19T00:34:44Z"
}

book

Emitted when the order book changes for a subscribed asset.

{
  "assetId": "AAPL",
  "bids": [
    { "price": 249.87, "size": 100 },
    { "price": 249.50, "size": 250 },
    { "price": 249.25, "size": 500 }
  ],
  "asks": [
    { "price": 250.01, "size": 100 },
    { "price": 250.25, "size": 150 },
    { "price": 250.50, "size": 300 }
  ],
  "timestamp": "2026-03-19T00:34:45Z"
}

trade

Emitted when a trade executes on a subscribed asset.

{
  "assetId": "AAPL",
  "tradeId": "trd_abc123",
  "price": 249.93,
  "size": 50,
  "side": "buy",
  "timestamp": "2026-03-19T00:34:44Z"
}

order (authenticated)

Emitted when the status of one of the user's orders changes. Requires authentication.

{
  "orderId": "ord_a1b2c3d4e5f6",
  "assetId": "AAPL",
  "side": "BUY",
  "orderType": "limit",
  "quantity": 10,
  "filledQuantity": 5,
  "averagePrice": 249.90,
  "status": "PARTIALLY_FILLED",
  "timestamp": "2026-03-19T00:35:02Z"
}

WebSocket (Legacy)

WebSocket is supported for backwards compatibility with existing integrations. New integrations should use SSE.

Connect

wss://api.{env}.satschel.com/ws

Message Format

All WebSocket messages use JSON:

{
  "type": "message_type",
  "data": {},
  "timestamp": 1710807360
}

Subscribe to Order Book

{
  "type": "subscribe",
  "channel": "orderbook",
  "symbol": "BTC-USD",
  "depth": 20
}

Response (snapshot):

{
  "type": "orderbook_snapshot",
  "symbol": "BTC-USD",
  "bids": [[83400, 1.5], [83399, 2.0]],
  "asks": [[83401, 1.2], [83402, 0.8]],
  "timestamp": 1710807360
}

Incremental updates:

{
  "type": "orderbook_update",
  "symbol": "BTC-USD",
  "bids": [[83400, 1.8]],
  "asks": [[83401, 0]],
  "timestamp": 1710807361
}

A size of 0 means the price level has been removed.

Subscribe to Trades

{
  "type": "subscribe",
  "channel": "trades",
  "symbol": "BTC-USD"
}

Response:

{
  "type": "trade",
  "symbol": "BTC-USD",
  "price": 83421.50,
  "size": 0.5,
  "side": "buy",
  "timestamp": 1710807360
}

Unsubscribe

{
  "type": "unsubscribe",
  "channel": "orderbook",
  "symbol": "BTC-USD"
}

WebSocket TypeScript Client

class RealtimeClient {
  private ws: WebSocket;
  private handlers: Map<string, ((data: any) => void)[]> = new Map();

  constructor(private url: string) {
    this.ws = new WebSocket(url);
    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      const callbacks = this.handlers.get(msg.type) ?? [];
      for (const cb of callbacks) {
        cb(msg);
      }
    };
  }

  subscribe(channel: string, symbol: string, depth?: number): void {
    this.ws.send(JSON.stringify({
      type: 'subscribe',
      channel,
      symbol,
      ...(depth ? { depth } : {}),
    }));
  }

  on(type: string, handler: (data: any) => void): void {
    const existing = this.handlers.get(type) ?? [];
    existing.push(handler);
    this.handlers.set(type, existing);
  }

  close(): void {
    this.ws.close();
  }
}

// Usage
const client = new RealtimeClient('wss://api.stage.satschel.com/ws');

client.subscribe('orderbook', 'BTC-USD', 20);
client.subscribe('trades', 'AAPL');

client.on('orderbook_snapshot', (msg) => {
  console.log(`${msg.symbol} book: ${msg.bids.length} bids, ${msg.asks.length} asks`);
});

client.on('trade', (msg) => {
  console.log(`${msg.symbol}: ${msg.size} @ ${msg.price}`);
});

WebSocket Error Handling

{
  "type": "error",
  "error": {
    "code": "SUBSCRIPTION_FAILED",
    "message": "Failed to subscribe to channel",
    "channel": "orderbook",
    "symbol": "INVALID-PAIR"
  }
}

Polling Fallback

If SSE and WebSocket are unavailable, use REST polling as a fallback:

DataEndpointInterval
QuoteGET /v1/assets/{id}/quote5 seconds
Order bookGET /v1/assets/{id}/book3 seconds
Order statusGET /v1/orders/{id}5 seconds

The frontend uses polling by default and upgrades to SSE when available.


Connection Limits

ChannelLimit
SSE connections per user5
WebSocket connections per user5
Subscriptions per connection50
Message rate (inbound, WebSocket)100 messages/second

On this page