Stockbit Research research Playground @hansputera

Trading Data WebSocket

Connection

wss://wss-trading.stockbit.com/ws
Optional query: ?type=chart
Sub-protocol: ["web"]

Authentication

Requires a WebSocket key obtained first from the REST API (see Authentication).

Subscription Protocol (Protobuf)

Request Schema

message WebsocketRequest {
    string user_id = 1;          // User ID (e.g. "3468390")
    WebsocketChannel channel = 2; // Subscription channels
    string key = 3;              // WebSocket key
    PingRequest ping = 4;        // PingRequest { string message = 1 } — sends "ping" every ~30s on chart WS
    string access_token = 5;     // Bearer access token
}

message WebsocketChannel {
    repeated string watchlist = 1;
    repeated string order_book = 2;
    repeated string running_trade = 3;
    bool is_hotlist = 4;
    repeated string running_trade_batch = 5;
    repeated string liveprice = 6;
    repeated string iepiev = 7;
    repeated string intraday = 8;
    repeated string best_bid_offer = 9;
    repeated MarketMoverWebsocketRequest market_mover = 10;
    repeated OrderQueueWebsocketRequest order_queue = 11;
    repeated string liveprice_v3 = 12;
    repeated string order_book_v3 = 13;
    repeated string intraday_v3 = 14;
}

Two-Step Handshake

The server expects two sequential protobuf messages on connect:

  1. Auth-only messageuser_id + key + access_token, no channel field
  2. Subscription messageuser_id + key + access_token + channel with desired stocks

Both are sent immediately without waiting for a response.

from stockbit_api.proto.stockbit_pb2 import WebsocketRequest

# Step 1: Auth-only (no channel)
auth = WebsocketRequest()
auth.user_id = "3468390"
auth.key = wskey
auth.access_token = "<bearer_token>"

# Step 2: Subscription with channels
sub = WebsocketRequest()
sub.user_id = "3468390"
sub.key = wskey
sub.access_token = "<bearer_token>"
sub.channel.liveprice.extend(["BBRI", "BBCA"])
sub.channel.running_trade_batch.extend(["BBRI"])
sub.channel.order_book.extend(["BBRI"])

await ws.send(auth.SerializeToString())
await ws.send(sub.SerializeToString())

Response Envelope

The server uses protobuf field numbers directly in a oneof envelope (not a WebsocketSubResponse with string case):

message WebsocketWrapMessage {
    oneof event {
        RunningTradeBatch running_trade = 8;   // Sub: running_trade_batch
        LivePrice liveprice = 9;               // Sub: liveprice
        OrderbookFeed orderbook = 10;           // Sub: order_book
    }
}
Outer Field Tag Byte Subscription Channel Description
8 (RunningTradeBatch) 0x4a running_trade_batch (field 5) Batched trade executions
9 (LivePrice) 0x52 liveprice (field 6) Real-time stock price tick
10 (OrderbookFeed) 0x5a order_book (field 2) Orderbook snapshot (text format)

Decode using WhichOneof("event"):

env = WebsocketWrapMessage()
env.ParseFromString(raw)
which = env.WhichOneof("event")

if which == "liveprice":
    lp = env.liveprice
    print(f"{lp.stock_code}: {lp.lastprice}")
elif which == "running_trade":
    for t in env.running_trade.trades:
        print(f"Trade {t.stock}: {t.price} {t.volume}")
elif which == "orderbook":
    ob = env.orderbook
    # Parse pipe-delimited text in ob.orderbook_data

OrderbookFeed — Text Format

The orderbook data at field 10 wraps a pipe-delimited text format in the orderbook_data field:

message OrderbookFeed {
    string symbol = 1;
    bytes orderbook_data = 2;   // Pipe-delimited text
    int64 sequence = 3;
    int32 board = 4;            // 722 = reguler
    string timestamp = 5;
    int64 flag = 8;
    string timestamp_ns = 9;
}

Format: #O|SYMBOL|OFFER|price;lot;volume|price;lot;volume|...#O|SYMBOL|BID|price;lot;volume|...

def parse_orderbook_text(data: bytes) -> dict:
    text = data.decode("utf-8", errors="replace")
    entries = text.split("|")
    result = {"bid": [], "offer": []}
    side = None
    for part in entries:
        if part in ("#O", "#B"):
            side = None
        elif part == "OFFER":
            side = "offer"
        elif part == "BID":
            side = "bid"
        elif side and ";" in part:
            parts = part.split(";")
            if len(parts) >= 3:
                try:
                    result[side].append({"price": float(parts[0]), "lot": float(parts[1]), "volume": float(parts[2])})
                except ValueError:
                    pass
    return result

Chart WebSocket (?type=chart)

URL: wss://wss-trading.stockbit.com/ws?type=chart

Subscription channels: liveprice (field 6) + iepiev (field 7)

sub = WebsocketRequest()
sub.user_id = uid
sub.key = wskey
sub.access_token = token
sub.channel.liveprice.append("BBCA")
sub.channel.iepiev.append("BBCA")

Periodic ping: The chart WS requires client-side pings every ~30 seconds:

ping = WebsocketRequest()
ping.ping.message = "ping"   # PingRequest { string message = 1 }
await ws.send(ping.SerializeToString())

The raw ping frame is IgYKBHBpbmc= (base64) / 22 06 0a 04 70 69 6e 67 (hex).

Quick Reference

import asyncio, websockets
from stockbit_api.proto.stockbit_pb2 import WebsocketRequest

async def connect(wskey, token, uid, stocks):
    auth = WebsocketRequest()
    auth.user_id = uid; auth.key = wskey; auth.access_token = token

    sub = WebsocketRequest()
    sub.user_id = uid; sub.key = wskey; sub.access_token = token
    sub.channel.liveprice.extend(stocks)
    sub.channel.running_trade_batch.extend(stocks)
    sub.channel.order_book.extend(stocks)

    ws = await websockets.connect("wss://wss-trading.stockbit.com/ws", subprotocols=["web"])
    await ws.send(auth.SerializeToString())
    await ws.send(sub.SerializeToString())

    async for msg in ws:
        # Parse with WebsocketWrapMessage
        pass