Overview
Field Description Topic publicTradeTemplateId 1003Format SBE binary frame (opcode = 2), little-endian Units Timestamps in microseconds (µs), but only accurate to milliseconds. Append 000 to a millisecond timestamp to get the microsecond format. E.g., millisecond timestamp 1726233600001 corresponds to µs timestamp 1726233600001000 Content Public platform trade records (batch push, a single frame may contain multiple trades) Update Frequency Real-time
Connection
WebSocket URL : wss://ws.bitget.com/v3/ws/public/sbe
Heartbeat : Send text frame "ping" every 30 seconds, server responds with "pong"
Message Protocol : JSON text frames during subscription phase; SBE binary frames during market data push phase, distinguished by WebSocket opCode (opCode=1 for text, opCode=2 for binary)
Subscription Flow
1. Send Subscription Request
{
"op" : "subscribe" ,
"args" : [
{
"instType" : "usdt-futures" ,
"topic" : "publicTrade" ,
"symbol" : "BTCUSDT"
}
]
}
Parameter Description:
Parameter Type Description instType string Product type: spot usdt-futures usdc-futures coin-futures topic string Fixed value: publicTrade symbol string Symbol name, e.g. BTCUSDT, ETHUSDT
2. Subscription Confirmation
{
"event" : "subscribe" ,
"arg" : {
"instType" : "usdt-futures" ,
"topic" : "publicTrade" ,
"symbol" : "BTCUSDT"
}
}
3. Receiving Data
After subscription confirmation, SBE binary frames are pushed in real-time whenever a trade occurs. A single frame may contain multiple trades (batch).
4. Unsubscribe
{
"op" : "unsubscribe" ,
"args" : [
{
"instType" : "usdt-futures" ,
"topic" : "publicTrade" ,
"symbol" : "BTCUSDT"
}
]
}
SBE Message Structure
Price/Size Calculation Formula
actual_value = mantissa × 10^exponent
Example : mantissa = 123456, exponent = -4, represents 12.3456 (actual_value = mantissa × 10 ^ exponent)
All SBE messages must include a fixed 8-byte header for parsing and identifying subsequent data.
Field Type Length (Byte) Description blockLength uint16 2 Root block length templateId uint16 2 Channel unique identifier, fixed value = 1003 schemaId uint16 2 Schema ID version uint16 2 Schema version
Message Field Definitions
Root block
id Field Type Description - messageHeader Composite Fixed header 1 priceExponent int8 Price exponent 2 sizeExponent int8 Size exponent 100 padding uint8 Padding bytes
Group: trades (id=200)
id Field Type Description 1 ts uint64 Matching engine timestamp µs timestamp, but only accurate to milliseconds. Append 000 to a millisecond timestamp to get the microsecond format. E.g., millisecond timestamp 1726233600001 → µs 1726233600001000 2 execId uint64 Execution ID 3 price int64 Trade price mantissa 4 size int64 Trade size mantissa 5 side uint8 Trade direction: Buy / Sell 6 isRPI uint8 (BooleanType) Retail Price Improvement flag: T / F (since schema version 4) 7 sts uint64 Stream service push timestamp in microseconds 8 category uint8 Product line: spot / usdt-futures / coin-futures / usdc-futures 50 padding uint8 Padding bytes
Data
id Field Type Description 300 symbol varString[8] Symbol name, UTF-8 format
Binary Layout Overview
┌─────────────┬─────────────┬──────────────────────────────────────────┬──────────┐
│ Header (8 B ) │ Root (8 B ) │ Trades : GrpHdr (4 B ) + N ×40 B │ Symbol │
└─────────────┴─────────────┴──────────────────────────────────────────┴──────────┘
Single trade message size : 8 + 8 + 4 + 40 + 1 + len(symbol) = approximately 69 bytes
N trade message size : 8 + 8 + 4 + 40×N + 1 + len(symbol)
Trade Direction (side)
Value Name Description 0 Buy Active buy (Taker buyer) 1 Sell Active sell (Taker seller)
Decoding Example
Raw Binary (single trade, hex)
08 00 EB 03 01 00 04 00 <- header : blockLength = 8 , templateId = 1003 , schemaId = 1 , version = 4
FE <- priceExponent = - 2
FC <- sizeExponent = - 4
00 00 00 00 00 00 <- padding6
28 00 01 00 <- trades group : entryBlockLength = 40 , numInGroup = 1
02 80 C6 A7 86 3 E 06 00 <- trades[ 0 ]. ts (uint64 LE )
0 F 27 00 00 00 00 00 00 <- trades[ 0 ].execId = 9999
52 39 64 00 00 00 00 00 <- trades[ 0 ].price = 6566738
88 13 00 00 00 00 00 00 <- trades[ 0 ].size = 5000
00 <- trades[ 0 ].side = 0 (Buy)
00 00 00 00 00 00 00 <- padding7
07 42 54 43 55 53 44 54 <- symbol : length = 7 , "BTCUSDT"
Decoded JSON
{
"header" : {
"block_length" : 8 ,
"template_id" : 1003 ,
"schema_id" : 1 ,
"version" : 4
},
"price_exponent" : - 2 ,
"size_exponent" : - 4 ,
"trades" : [
{
"ts" : 1700000000000002 ,
"exec_id" : 9999 ,
"price" : "65667.38" ,
"size" : "0.5000" ,
"side" : "Buy" ,
"isRPI" : "F" ,
"sts" : 1700000000001002 ,
"category" : 1
}
],
"symbol" : "BTCUSDT"
}
Batch Trade Decoded JSON (multiple trades)
{
"header" : {
"block_length" : 8 ,
"template_id" : 1003 ,
"schema_id" : 1 ,
"version" : 4
},
"price_exponent" : - 2 ,
"size_exponent" : - 4 ,
"trades" : [
{
"ts" : 1700000000000002 ,
"exec_id" : 10001 ,
"price" : "65665.78" ,
"size" : "0.5000" ,
"side" : "Buy" ,
"isRPI" : "F" ,
"sts" : 1700000000001002 ,
"category" : 1
},
{
"ts" : 1700000000000003 ,
"exec_id" : 10002 ,
"price" : "65666.00" ,
"size" : "1.2000" ,
"side" : "Sell" ,
"isRPI" : "T" ,
"sts" : 1700000000001003 ,
"category" : 1
},
{
"ts" : 1700000000000004 ,
"exec_id" : 10003 ,
"price" : "65664.50" ,
"size" : "0.3000" ,
"side" : "Buy" ,
"isRPI" : "F" ,
"sts" : 1700000000001004 ,
"category" : 1
}
],
"symbol" : "BTCUSDT"
}
Python Integration Example
"""Bitget publicTrade SBE WebSocket subscription example"""
import asyncio
import json
import struct
from decimal import Decimal
import websockets
WS_URL = "wss://ws.bitget.com/v3/ws/public/sbe"
INST_TYPE = "usdt-futures"
SYMBOL = "BTCUSDT"
TOPIC = "publicTrade"
SIDE_MAP = {0: "Buy" , 1: "Sell" }
BOOLEAN_MAP = {0: "F" , 1: "T" }
def decode_trade(data: bytes) -> dict:
"""Decode Trade (templateId=1003) SBE frame"""
block_length, template_id, schema_id, version = struct.unpack_from( '<HHHH' , data, 0)
assert template_id == 1003, f "unexpected templateId: {template_id}"
offset = 8
base = offset
price_exp, = struct.unpack_from( '<b' , data, offset); offset += 1
size_exp, = struct.unpack_from( '<b' , data, offset); offset += 1
# Skip padding, based on blockLength
offset = base + block_length
# trades group
entry_bl, num = struct.unpack_from( '<HH' , data, offset); offset += 4
has_is_rpi = version >= 4 # sinceVersion=4: entry carries the isRPI field (entryBlockLength stays fixed due to padding, so gate on schema version instead)
to_dec = lambda m, e: Decimal(m) * Decimal(10) ** e
trades = []
for _ in range(num):
entry_start = offset
ts, = struct.unpack_from( '<Q' , data, offset); offset += 8
exec_id, = struct.unpack_from( '<Q' , data, offset); offset += 8
price, = struct.unpack_from( '<q' , data, offset); offset += 8
size, = struct.unpack_from( '<q' , data, offset); offset += 8
side_raw, = struct.unpack_from( '<B' , data, offset); offset += 1
if has_is_rpi:
is_rpi_raw, = struct.unpack_from( '<B' , data, offset); offset += 1
sts, = struct.unpack_from( '<Q' , data, offset); offset += 8
category, = struct.unpack_from( '<B' , data, offset); offset += 1
# Skip padding, based on entry_bl
offset = entry_start + entry_bl
trade = {
"ts" : ts,
"exec_id" : exec_id,
"price" : str(to_dec(price, price_exp)),
"size" : str(to_dec(size, size_exp)),
"side" : SIDE_MAP.get(side_raw, str(side_raw)),
}
if has_is_rpi:
trade[ "isRPI" ] = BOOLEAN_MAP.get(is_rpi_raw, str(is_rpi_raw))
trade[ "sts" ] = sts
trade[ "category" ] = category
trades.append(trade)
# symbol (varString8)
sym_len, = struct.unpack_from( '<B' , data, offset); offset += 1
symbol = data[offset:offset + sym_len].decode( 'utf-8' )
return {
"price_exponent" : price_exp,
"size_exponent" : size_exp,
"trades" : trades,
"symbol" : symbol,
}
async def main():
async with websockets.connect(WS_URL) as ws:
# Subscribe
await ws.send(json.dumps({
"op" : "subscribe" ,
"args" : [{ "instType" : INST_TYPE, "topic" : TOPIC, "symbol" : SYMBOL}]
}))
print(f "[SUB] {INST_TYPE} {TOPIC} {SYMBOL}" )
# Heartbeat
async def ping_loop():
while True:
await asyncio.sleep(20)
await ws.send( "ping" )
print( "[PING] sent" )
asyncio.create_task(ping_loop())
async for message in ws:
if isinstance(message, bytes):
try:
msg = decode_trade(message)
print(f " \n [Trade] {msg['symbol']} ({len(msg['trades'])} trades)" )
for t in msg[ 'trades' ]:
ts_ms = t[ 'ts' ] // 1000
print(f " [{t['side']:4s}] price={t['price']} size={t['size']} "
f "ts={ts_ms}ms execId={t['exec_id']}" )
except Exception as e:
print(f "[ERROR] {e} raw={message.hex()}" )
else:
if message == "pong" :
print( "[PONG] received" )
else:
print(f "[TEXT] {message}" )
if __name__ == "__main__" :
asyncio.run(main())