Realtime

WebSocket stream

Live trading quotes and account events for Trading API keys over one socket at wss://api.your-brand.com/rt/v1/ws. Text frames, JSON, batched quotes, 15-minute sessions.

#Connecting

  1. Get a ticket

    A ticket is single-use and valid for 60 seconds. A read-only key is enough. Never put the API key itself in the socket URL.

    POST /api/open/v1/stream_ticket
    curl -X POST "https://api.your-brand.com/api/open/v1/stream_ticket" \
      -H "X-API-Key: $OCTOLIT_API_KEY"
    200 OK
    {
      "ticket": "st_...",
      "exp": 1,
      "expires_in": 60,
      "url": "https://trade.your-brand.com/login?token=...",
      "single_use": true
    }
  2. Open the socket

    Connect to wss://api.your-brand.com/rt/v1/ws on the same host as the REST API with either ?ticket=<ticket> in the URL or the header Authorization: Bearer <ticket>. The first frame you receive is welcome.

  3. Subscribe

    Send subscribe with up to 100 symbols (symbol codes as in GET /symbols). Account events for all your trading accounts arrive without subscribing.

    Minimal client
    const res = await fetch('https://api.your-brand.com/api/open/v1/stream_ticket', { method: 'POST', headers: { 'X-API-Key': process.env.OCTOLIT_API_KEY } });
    const { ticket } = await res.json();
    
    const ws = new WebSocket('wss://api.your-brand.com/rt/v1/ws?ticket=' + encodeURIComponent(ticket));
    ws.onopen = () => ws.send(JSON.stringify({ op: 'subscribe', symbols: ['EUR-USD', 'XAU-USD'] }));
    ws.onmessage = (e) => {
      const m = JSON.parse(e.data);
      if (m.type === 'quotes') for (const q of m.q) console.log(q.symbol, q.bid, q.ask);
      if (m.type === 'account') refreshPositions(m.account_id);     // fetch over REST
      if (m.type === 'closing' && m.reason === 'lifetime') reconnect(); // new ticket, new socket
    };

#Client → server

opFrame
subscribe{ "op": "subscribe", "symbols": ["EUR-USD", "XAU-USD"] }Start quotes for the pairs; answered with subscribed.
unsubscribe{ "op": "unsubscribe", "symbols": ["EUR-USD"] }Stop quotes for the pairs; answered with unsubscribed.
ping{ "op": "ping" }Answered with pong.

#Server → client

typeFrame
welcome{ "type": "welcome", "client_id": 1042, "accounts": [55], "limits": { "max_symbols": 100 } }First message after the handshake.
subscribed{ "type": "subscribed", "symbols": ["EUR-USD"] }The pairs now streaming.
unsubscribed{ "type": "unsubscribed", "symbols": ["EUR-USD"] }
quotes{ "type": "quotes", "q": [{ "symbol": "EUR-USD", "bid": 1.08452, "ask": 1.08461, "ts": 1762862400123 }] }Trading quotes (the prices orders fill at), batched.
account{ "type": "account", "event": "positions", "account_id": 55 }Something changed on one of your accounts (positions | balance): fetch it over REST.
status{ "type": "status", "quotes": "paused" }live | paused | unavailable - the quote feed state.
heartbeat{ "type": "heartbeat", "ts": 1762862400123 }Every 15 s; no reply needed.
pong{ "type": "pong" }
error{ "type": "error", "code": "bad_symbols", "symbols": ["FOO-BAR"] }bad_request | bad_symbols | too_many_symbols | unknown_op | rate_limited. More than 20 errors close the connection.
closing{ "type": "closing", "reason": "lifetime" }Sent right before the server closes.
Quotes are batched
A quotes frame carries the latest price of every changed symbol since the previous frame, at most a few frames a second. ts is Unix milliseconds. Use the WebSocket for display and triggers; use GET /quotes for a one-off read.

#Limits

Connections per client5
Connections per key3
Connections per address50
Symbols per connection100
Client messages per second10 (burst 20)
Connection lifetime15 min, then close 1000 lifetime - reconnect with a new ticket
Heartbeatevery 15 s (server -> client)
Client message size4 096 bytes

#Close reasons

Before closing, the server sends { "type": "closing", "reason": "…" } and then closes with the code below.

1008key_revokedThe key was revoked.
1008key_expiredThe key expired.
1008access_offAPI access or the stream was switched off.
1000lifetimeThe 15-minute lifetime ended - reconnect with a new ticket.
1001shutting_downServer restart - reconnect.
1008slow_consumerYour side did not read fast enough.
1008too_many_errorsMore than 20 error messages.
1002protocol_errorMalformed frame.
1009message_too_bigA client message over 4 096 bytes.
1007invalid_utf8Text frame is not UTF-8.

#Patterns

  • Reconnect: on lifetime or shutting_down fetch a new ticket and reconnect with backoff (1 s, 2 s, 4 s … 30 s). Re-send your subscribe.
  • Account events are signals, not state: on account fetch GET /positions or GET /trading_accounts/{id}. You never miss a change because of a dropped frame.
  • Status: quotes: "paused" means the broker paused prices - keep the socket, show stale.
  • One socket per process: subscribe to all symbols on one connection instead of many sockets; limits are per client and per key.