WebSocket API

Real-time crypto derivatives data streaming

AsyncAPI Spec

Connection

Two protocols are supported. Choose based on your needs:

RECOMMENDED Native WebSocket
Endpoint
wss://apiv2.laevitas.ch/ws
Protocol
Native WebSocket (W3C)
Authentication
apikey: header (or `auth` RPC)

Industry standard - same protocol used by Deribit, Binance, OKX, Bybit, Hyperliquid

LEGACY Socket.IO
Endpoint
wss://apiv2.laevitas.ch/stream
Protocol
Socket.IO v4
Authentication
auth: { apiKey: 'your-key' }

Auto-reconnect, acknowledgements - requires Socket.IO client library

Limits & Close Codes

Per-connection
Subscriptions 200
Inbound messages / sec 20
Max lifetime 24 h
Slow-consumer threshold 2 MB queued
Per-source
Concurrent connections per API key 5
Concurrent connections per IP 20

Need higher caps? Contact [email protected] with your use case.

Close codes
1001 Server going away (graceful shutdown)
4001 Auth failed — surface to user, no auto-retry
4002 Idle timeout (pong missed) — reconnect with backoff
4003 Slow consumer — drain faster or reduce subscriptions
4004 24 h lifetime cap reached — reconnect
4005 Too many concurrent connections (per key or IP)
4006 No active streaming pass — buy a pass and reconnect (x402 only)
4008 Inbound message rate exceeded

Best practices

The gateway behaves like industry-standard exchange feeds (Deribit, Binance, OKX, Bybit). The patterns below are the same ones a hardened exchange client uses — calibrated to the exact server-side timers and limits documented above.

Reconnect strategy: dispatch by close code

Don't blindly reconnect on every disconnect. The close code tells you exactly what happened — treat each one differently:

1006 / 1011 Network hiccup, abnormal close. Reconnect with exponential backoff (1s → 2s → 4s → ... cap at 30s, jitter ±20%). Resubscribe to your channels.
1001 Server graceful shutdown (deploy / restart). Reconnect after 1–5s — the new pod is usually up by then. No backoff needed.
4001 Auth failed. Don't auto-retry. Surface to the user — key is missing, invalid, revoked, or your credit-token is bad. Reconnecting with the same credentials will fail identically.
4002 Idle timeout (you missed a pong). Your client wasn't replying to pings. Reconnect immediately and check that your event loop isn't blocked. Most native WS clients auto-pong; if you see this, suspect a stalled handler.
4003 Slow consumer. You couldn't drain incoming messages fast enough (server queued >2 MB to your socket). Don't blindly reconnect — you'll hit it again. Reduce subscriptions, batch processing, or move to a faster network. Then reconnect.
4004 24h lifetime cap. Industry-standard hygiene. Reconnect immediately and resubscribe. Build this into your client — it's not an error, it's an expected event roughly once per day.
4005 Too many concurrent connections (5 per API key, 20 per IP). Don't reconnect — close another connection first or contact support to raise your cap.
4006 x402 pass expired or absent. Buy a new pass via POST /api/v1/x402/ws-pass/{hour|day}, then reconnect with the returned credit token.
4008 Inbound rate limit (>20 messages/sec). Throttle your subscribe calls or batch them. Reconnect after a short pause; don't loop.
Always do
  • Persist your subscription list locally — on every reconnect, resubscribe to everything (the server has no memory of prior sessions).
  • Implement exponential backoff with jitter for codes 1006/1011. Without jitter, a fleet of clients all reconnecting at the same offset will dogpile the server after every restart.
  • Plan for the 24h disconnect (4004). If you stagger your connect time across replicas, your fleet won't all rotate at the same minute.
  • Pong promptly. Standard ws/socket.io clients do this automatically. If you write your own client, send pong within 75 s of every ping (server pings every 25 s).
  • Bundle your subscribes — one subscribe call with N channels rather than N calls with one channel each. Stays well under the 20/sec inbound rate limit.
  • Log the close code on every disconnect. Without it you can't tell a 4003 from a 4004 from a network blip.
Never do
  • Don't reconnect on 4001, 4003, or 4005 without first fixing the underlying problem. Auto-retry will produce the same error and burn your connection cap.
  • Don't tight-loop reconnect. No backoff = you trigger 4005 within seconds and then can't reconnect at all until the cap window clears.
  • Don't open one connection per channel. One connection holds up to 200 subscriptions. Five connections per API key cap.
  • Don't subscribe to firehoses you can't drain. book.*, trades.*, or broad reference-data wildcards can deliver thousands of events per second — if your handler blocks, you'll hit 4003 in under a minute.
  • Don't send the API key in the URL query string. It ends up in access logs and proxy traces. Use the apikey header instead.
  • Don't poll ping as a heartbeat. The server pings you; just respond with pong. The JSON-RPC {"method":"ping"} exists for round-trip latency probes, not liveness.
Reference reconnect loop (Node.js, native ws)
import WebSocket from 'ws';

const URL = 'wss://apiv2.laevitas.ch/ws';
const API_KEY = process.env.LAEVITAS_API_KEY;
const SUBSCRIPTIONS = [
  'trades.spot.binance.BTCUSDT',
  'trades.perpetuals.binance.BTCUSDT',
  'open-interest.perpetuals.bybit.BTCUSDT',
  'funding-rate.perpetuals.bybit.BTCUSDT',
];

// Codes that mean "the same retry will fail the same way" — surface and stop.
const FATAL = new Set([4001, 4003, 4005, 4006]);

let attempt = 0;

function connect() {
  const ws = new WebSocket(URL, { headers: { apikey: API_KEY } });

  ws.on('open', () => {
    attempt = 0; // reset backoff on successful connect
    ws.send(JSON.stringify({ id: 1, method: 'subscribe', params: { channels: SUBSCRIPTIONS } }));
  });

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    if (msg.channel) handleEvent(msg.channel, msg.data);
  });

  ws.on('close', (code, reason) => {
    console.log(`closed code=${code} reason=${reason}`);
    if (FATAL.has(code)) {
      console.error('Fatal close — not reconnecting. Fix the underlying issue.');
      return;
    }
    // Exponential backoff with jitter; cap at 30s.
    const base = Math.min(1000 * 2 ** attempt++, 30_000);
    const jitter = base * (0.8 + Math.random() * 0.4);
    setTimeout(connect, jitter);
  });

  ws.on('error', (err) => console.error('ws error:', err.message));
}

connect();

~30 lines. Handles all the failure modes documented above. Substitute SUBSCRIPTIONS with whatever you need; pass an X-Credit-Token header instead of apikey for x402 ws-pass auth.

x402 ws-pass clients: extra rule for 4006

A 4006 close means your pass expired (or was never active). Don't add 4006 to your fatal set blindly — instead, on 4006: call POST /api/v1/x402/ws-pass/hour (or /day) to buy a new pass, store the new x-credit-token, then reconnect. Treat it as a "refill required" signal, not a fatal error.

Streaming passes (x402)

New to x402? It's a USDC micropayment protocol that lets wallet customers access paid endpoints without an API key. The full model — credit-token JWTs, on-chain settlement via Base mainnet, supported networks, the REST credit-bundle flow that backs streaming passes — is documented at /x402. Read that first if any of the terms below feel unfamiliar.

Pay-as-you-go WebSocket access for x402 wallet customers. API-key customers are unaffected — your subscription continues to include unlimited streaming. The pass model is a time-slot reservation: you pay once for hour or day access, the server records your wallet's expiry in Redis, and you can connect / subscribe / disconnect / reconnect freely until the slot ends. There is no per-event metering, no refund machinery, and no automated SLA.

1-hour pass
$0.50
POST /api/v1/x402/ws-pass/hour
1-day pass
$10.00
POST /api/v1/x402/ws-pass/day
How it works
  1. POST to one of the pass routes — receive a 402 with USDC payment requirements (Base mainnet).
  2. Sign and retry with the PAYMENT-SIGNATURE header. The server settles on-chain, grants your slot, and returns an x-credit-token JWT.
  3. Connect to wss://apiv2.laevitas.ch/ws with that JWT in the X-Credit-Token header.
  4. The server validates your wallet has an active slot. Disconnect and reconnect freely until expiry.
  5. When the slot expires, your next connection attempt returns close code 4006. Buy another pass to resume.
  6. If your connection is still open at the expiry timestamp, the server closes it with the same 4006 code — a long-lived connection cannot outlive its paid window.
Stacking is additive

Buying a second pass while one is active extends your expiry by the new pass's duration. A day pass on top of a 30-min-remaining hour pass leaves you with 24h 30min remaining. Passes accumulate; nothing is lost.

What's NOT charged
  • Authentication failures (401, 4001, 4006)
  • Connection cap or rate-limit rejections (4005, 4008)
  • Server-initiated lifetime closes (4004) — passes survive across reconnects
  • Server restarts — passes survive
Pricing comparison
Tier Price Streaming included Monthly equivalent (24/7)
WS hour pass $0.50 1 hour $360
WS day pass $10.00 24 hours $300
API subscription $500/month unlimited (REST + WS) $500

For sustained heavy usage, the API subscription remains the most cost-effective option (rate-limit-free, full historical access, support included). Day passes are convenience pricing for short bursts — not a subscription replacement.

Service interruptions

Passes are non-refundable in USDC. If a service issue affects your pass, contact [email protected] — we may extend your pass or grant REST credits at our discretion. There is no automated SLA or compensation threshold.

Channels

Live Trades
trades.{market}.{exchange}.{instrument}

Real-time executed trades with strategy detection. Use the dedicated reference-data channels for OI and funding.

{market} perpetuals futures options spot predictions
{exchange} binance, deribit, okx, bybit, hyperliquid, coinbase, kraken, polymarket
{instrument} Instrument name
OHLC Ticker
ohlc.ticker.{market}.{exchange}.{instrument}.{tf}

Quote-based OHLC bars. Futures/options include mark/index prices, OI, and IV (options). Spot includes last_price, bid/ask spread, sizes, and 24h rolling stats. Predictions include probability OHLC plus bid/ask price, size, and spread OHLC.

{market} perpetuals futures options spot predictions
{tf} 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d
OHLC VT (Volume)
ohlc.vt.{market}.{exchange}.{instrument}.{tf}

Trade-based OHLC with volume breakdown and VWAP.

{market} perpetuals futures options spot predictions
{tf} 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d
Open Interest
open-interest.{market}.{exchange}.{instrument}

Real-time OI updates from raw venue ticker/open-interest streams without the full ticker candle payload.

{market} perpetuals futures options
{exchange} Venue-dependent; use the same exchange names as the corresponding futures/options catalog.
{instrument} Instrument name
Funding Rate
funding-rate.perpetuals.{exchange}.{instrument}

Dedicated perpetual funding updates with current rate, optional 8h-normalized rate, next funding fields, and mark/index price when supplied by the venue.

{market} perpetuals
{exchange} bybit, okx, binance, deribit, hyperliquid, kraken, nado where funding is available
{instrument} Instrument name
Liquidations
liquidations.{market}.{exchange}.{instrument}

Forced-liquidation events (margin calls, ADL) with price, side, USD-denominated size, mark + index price. The venue's raw payload is intentionally omitted — available via the corresponding REST endpoint.

{market} perpetuals futures
{exchange} binance, bybit, okx, kraken, nado
{instrument} Instrument name
Order Book (L2)
book.{market}.{exchange}.{instrument}

Full L2 snapshots (bids + asks, up to 100 levels per side). Each event is a complete snapshot — no deltas, no sequence reconstruction. Includes pre-computed cumulative liquidity at depth tiers (10/20/50/100), imbalance ratios, and microprice for cascade / imbalance detection.

{market} perpetuals futures spot predictions (no options — venues don't expose L2)
{exchange} binance, bybit, okx, hyperliquid, coinbase, kraken, polymarket
{instrument} Instrument name
Perpetuals split — breaking change in 1.27

Perpetual swaps live under their own market namespace: trades.perpetuals.{exchange}.{instrument} and ohlc.{ticker,vt}.perpetuals.…. The pre-1.27 alias that also surfaced perp events on trades.futures.… / ohlc.…futures.… has been removed — those channels are now dated-futures only. If your client was relying on the alias, switch perp subscriptions to the perpetuals market.

Predictions instruments are short-lived

Polymarket prediction markets resolve and expire fast — sports / event-driven instruments can disappear within hours. Don't hard-code instrument slugs in client integrations. Discover live instruments via /api/v1/predictions/catalog (filter by category / currency) and browse available categories at /api/v1/predictions/categories.

Macro perpetuals stream over the same channels

Equity perps (NVDA, TSLA, MSTR, AAPL, …), commodity perps (GOLD, OIL/CL, SILVER, NATGAS, …), FX perps (EURUSD, GBPUSD, JPYUSD) and the small index segment all publish on trades.perpetuals.…, ohlc.{ticker,vt}.perpetuals.…, liquidations.perpetuals.…, book.perpetuals.…, open-interest.perpetuals.…, and funding-rate.perpetuals.… — same shape as crypto perps. Discover instruments and venues via /api/v1/macro/catalog.

Examples: trades.perpetuals.kraken.PF_NVDAXUSD, ohlc.ticker.perpetuals.hyperliquid.xyz:NVDA-USD.1m, trades.perpetuals.binance.XAUUSDT, book.perpetuals.hyperliquid.flx:GOLD-USD, open-interest.perpetuals.bybit.XAUTUSDT. Hyperliquid HIP-3 sub-venues (xyz, flx, km, cash, vntl, hyna) are colon-prefixed in the instrument name. Equity / FX perps only tick during the underlying market's hours on some venues (Kraken PF_*, Hyperliquid cash:*); 24/7 venues like Hyperliquid xyz:* and Binance *USDT stream continuously.

Wildcards: subscribe to many channels at once

Use * as a wildcard in the market, exchange, or instrument position to subscribe to multiple channels with a single request. Trailing * auto-pads remaining segments — trades.* is shorthand for trades.*.*.*.

Examples: trades.*, liquidations.perpetuals.*, book.perpetuals.*.BTCUSDT, open-interest.*, funding-rate.perpetuals.*, trades.predictions.polymarket.*, ohlc.ticker.*.binance.BTCUSDT.1m.

Wire format: each event arrives with the channel field set to the resolved concrete path (e.g. trades.spot.binance.BTCUSDT), not the wildcard pattern you subscribed to. Client dispatch keys off the concrete path the same way it does for non-wildcard subs.

Constraints: * is rejected in the {channel} position (pick a channel type), in OHLC {dataType} (pick ticker or vt), and in OHLC {timeframe} (pick a timeframe). Wildcards count as 1 against the 200-sub per-connection cap. High-volume firehose subs (e.g. book.*) need a fast consumer — if the outbound buffer fills you'll be closed with code 4003.

Payload Examples

Perpetuals Trade Event

{
  "trade_id": "401318266",
  "instrument_name": "BTC-PERPETUAL",
  "exchange": "deribit",
  "direction": "sell",
  "price": 93230,
  "amount": 9000,
  "contracts": 900,
  "mark_price": 93220.37,
  "index_price": 91523.43,
  "open_interest": 328996550,
  "oi_change": 1390
}

Options Trade Event — example expiry; see /options/catalog for current

{
  "trade_id": "401321563",
  "instrument_name": "BTC-26MAR27-100000-C",
  "strike": 100000,
  "option_type": "C",
  "direction": "sell",
  "price": 0.0426,
  "premium_usd": 48894.91,
  "iv": 43.98,
  "delta": -4.437,
  "strategy": "BULL_DIAGONAL_SPREAD"
}

OHLC Ticker Event

{
  "state": "live",
  "instrument_name": "BTC-PERPETUAL",
  "timeframe": "5m",
  "mark_price_open": 93322.9,
  "mark_price_high": 93326.78,
  "mark_price_low": 93284.12,
  "mark_price_close": 93294.52,
  "oi_close": 3525.95
}

Open Interest Event

{
  "timestamp": 1777464961535,
  "exchange": "bybit",
  "instrument_name": "BTCUSDT",
  "instrument_type": "perpetual",
  "currency": "BTC",
  "open_interest": 91234.5,
  "oi_before": 91102.1,
  "oi_change": 132.4,
  "mark_price": 89990,
  "index_price": 89980,
  "source": "ticker"
}

Funding Rate Event

{
  "timestamp": 1777464961535,
  "exchange": "bybit",
  "instrument_name": "BTCUSDT",
  "instrument_type": "perpetual",
  "currency": "BTC",
  "funding_rate": 0.0001,
  "funding_8h": 0.0001,
  "next_funding_rate": 0.00012,
  "next_funding_time": 1777478400000,
  "mark_price": 89990,
  "index_price": 89980,
  "source": "ticker"
}

OHLC VT Event

{
  "state": "live",
  "instrument_name": "BTC-PERPETUAL",
  "timeframe": "5m",
  "open": 93302.5,
  "high": 93317.5,
  "low": 93302.5,
  "close": 93317.5,
  "vwap": 93306.59,
  "buy_volume": 2030,
  "sell_volume": 760
}

Liquidation Event

{
  "timestamp": 1777461297250,
  "exchange": "binance",
  "instrument_name": "BTCUSDT",
  "instrument_type": "perpetual",
  "direction": "buy",
  "position_side": "short",
  "category": "forced",
  "price": 90000,
  "amount": 0.5,
  "amount_usd": 45000,
  "mark_price": 89990,
  "index_price": 89980
}

Order Book Snapshot

{
  "timestamp": 1777464961535,
  "exchange": "bybit",
  "instrument_name": "BTCUSDT",
  "instrument_type": "perpetual",
  "depth": 100,
  "bids": [[89990, 1.5], [89989, 0.8]],
  "asks": [[89991, 0.6], [89992, 2.1]],
  "bid_liquidity_10": 1512918.4,
  "ask_liquidity_10": 1212726.78,
  "imbalance_10": 0.11,
  "microprice": 89990.78
}

Operations

NATIVE WS JSON-RPC style (like Deribit/Binance)

Subscribe

{
  "id": 1,
  "method": "subscribe",
  "params": {
    "channels": [
      "trades.perpetuals.binance.BTCUSDT",
      "open-interest.perpetuals.bybit.BTCUSDT",
      "funding-rate.perpetuals.bybit.BTCUSDT"
    ]
  }
}

Response

{
  "id": 1,
  "result": {
    "subscriptionIds": ["sub_123", "sub_124", "sub_125"],
    "channels": [
      "trades.perpetuals.binance.BTCUSDT",
      "open-interest.perpetuals.bybit.BTCUSDT",
      "funding-rate.perpetuals.bybit.BTCUSDT"
    ]
  }
}

Data Event

{
  "channel": "open-interest.perpetuals.bybit.BTCUSDT",
  "data": { "open_interest": 91234.5, "oi_change": 132.4, ... }
}

Unsubscribe

{
  "id": 2,
  "method": "unsubscribe",
  "params": { "subscriptionId": "sub_123" }
}
SOCKET.IO Event-based with callbacks

Subscribe

socket.emit('subscribe', {
  channel: 'open-interest.perpetuals.bybit.BTCUSDT'
}, (response) => {
  console.log('ID:', response.subscriptionId);
});

Unsubscribe

socket.emit('unsubscribe', {
  subscriptionId: 'sub-123'
});

Connection

Disconnected

Subscribe

Active Subscriptions

No active subscriptions

Events

Connect and subscribe to see events

Events: 0 Rate: 0/s
RECOMMENDED

Native WebSocket Examples

JavaScript / Node.js

const WebSocket = require('ws');

// Server-side: pass the key as a header. In browsers (where headers aren't
// allowed on WebSocket), send an `auth` RPC as the first message instead:
//   ws.send(JSON.stringify({ id: 'auth-1', method: 'auth', params: { apiKey } }))
const ws = new WebSocket('wss://apiv2.laevitas.ch/ws', {
    headers: { apikey: 'your-api-key' },
});

ws.on('open', () => {
    console.log('Connected to WebSocket');

    // Subscribe to a concrete channel + a wildcard pattern in one request.
    // Wildcards (`*`) work in market / exchange / instrument positions.
    ws.send(JSON.stringify({
        id: 1,
        method: 'subscribe',
        params: {
            channels: [
                'trades.perpetuals.binance.BTCUSDT',  // one concrete channel
                'liquidations.perpetuals.*',          // every perp liquidation across exchanges
                'open-interest.perpetuals.bybit.BTCUSDT',
                'funding-rate.perpetuals.bybit.BTCUSDT',
            ],
        },
    }));
});

ws.on('message', (data) => {
    const msg = JSON.parse(data);

    // Handle subscription response
    if (msg.id === 1 && msg.result) {
        console.log('Subscribed:', msg.result.subscriptionIds);
    }

    // Handle data events
    if (msg.channel && msg.data) {
        if (msg.channel.startsWith('open-interest.')) {
            const oi = msg.data;
            console.log(`OI: ${oi.exchange} ${oi.instrument_name} ${oi.open_interest}`);
        } else if (msg.channel.startsWith('funding-rate.')) {
            const funding = msg.data;
            console.log(`Funding: ${funding.exchange} ${funding.instrument_name} ${funding.funding_rate}`);
        } else {
            console.log(msg.channel, msg.data);
        }
    }
});

ws.on('close', () => console.log('Disconnected'));

Python

import websockets
import asyncio
import json

async def main():
    uri = 'wss://apiv2.laevitas.ch/ws'
    headers = {'apikey': 'your-api-key'}

    async with websockets.connect(uri, additional_headers=headers) as ws:
        print('Connected to WebSocket')

        # Subscribe to a concrete channel + a wildcard pattern in one request.
        # Wildcards (`*`) work in market / exchange / instrument positions.
        await ws.send(json.dumps({
            'id': 1,
            'method': 'subscribe',
            'params': {'channels': [
                'trades.perpetuals.binance.BTCUSDT',   # one concrete channel
                'liquidations.perpetuals.*',           # every perp liquidation across exchanges
                'open-interest.perpetuals.bybit.BTCUSDT',
                'funding-rate.perpetuals.bybit.BTCUSDT',
            ]},
        }))

        async for message in ws:
            msg = json.loads(message)

            # Handle subscription response
            if msg.get('id') == 1 and msg.get('result'):
                print(f"Subscribed: {msg['result']['subscriptionIds']}")

            # Handle data events
            if 'channel' in msg and 'data' in msg:
                channel = msg['channel']
                payload = msg['data']
                if channel.startswith('open-interest.'):
                    print(f"OI: {payload['exchange']} {payload['instrument_name']} {payload['open_interest']}")
                elif channel.startswith('funding-rate.'):
                    print(f"Funding: {payload['exchange']} {payload['instrument_name']} {payload['funding_rate']}")
                else:
                    print(channel, payload)

asyncio.run(main())
LEGACY

Socket.IO Examples

JavaScript / Node.js

const io = require('socket.io-client');

const socket = io('wss://apiv2.laevitas.ch/stream', {
    transports: ['websocket'],
    auth: { apiKey: 'your-api-key' }
});

socket.on('connect', () => {
    console.log('Connected to WebSocket');

    // Subscribe to BTC open interest without the full ticker payload.
    const channel = 'open-interest.perpetuals.bybit.BTCUSDT';
    socket.emit('subscribe', { channel }, (response) => {
        console.log('Subscribed:', response.subscriptionId);
    });

    socket.on(channel, (data) => {
        console.log(`OI: ${data.open_interest} source=${data.source}`);
    });
});

socket.on('disconnect', () => console.log('Disconnected'));

Python

import socketio

sio = socketio.Client()

@sio.event
def connect():
    print('Connected to WebSocket')

    # Subscribe to BTC open interest without the full ticker payload.
    channel = 'open-interest.perpetuals.bybit.BTCUSDT'
    sio.emit('subscribe', {'channel': channel},
             callback=lambda r: print(f"Subscribed: {r['subscriptionId']}"))

@sio.on('open-interest.perpetuals.bybit.BTCUSDT')
def on_open_interest(data):
    print(f"OI: {data['open_interest']} source={data['source']}")

@sio.event
def disconnect():
    print('Disconnected')

sio.connect('wss://apiv2.laevitas.ch/stream',
            auth={'apiKey': 'your-api-key'},
            transports=['websocket'])
sio.wait()