Skip to Content
Zh CnDocs
行情 API
WebSocket 数据服务

WebSocket 数据服务

WebSocket 是官方推荐的实时行情接入方式。一条长连接可订阅多类数据:链上成交、K 线、Token 元数据、RWA 行情等。

API Key 申请与获取方式见 鉴权说明

数据能力边界

重要说明: WebSocket 仅提供实时数据推送能力,网关不为客户端缓存或回放历史事件。断线期间产生的事件不会补发;重连后须重新 auth + subscribe,只能从后续 event 流继续接收。

如需获取历史数据(历史 K 线、历史成交、Token 快照等),请使用 HTTP 行情 API 拉取。RWA 历史数据见 RWA 行情

能力WebSocketHTTP API
实时推送支持不支持(需轮询)
历史数据不支持支持
断线补发不支持,断线期间事件丢失可按时间范围查询

推荐接入模式:

  1. 启动时 — 通过 HTTP API 拉取所需历史数据(如最近 N 根 K 线、Token 基础信息)。
  2. 运行中 — 通过 WebSocket 订阅实时 event,增量更新本地状态。
  3. 重连后 — 再次通过 HTTP 补全断线窗口内的缺失数据,然后重新 subscribe 继续接收实时流。

连接限制

  • WebSocket 连接建立后 5 秒内须发送 auth,否则服务端主动断开。
  • 每个 partner 最多 5 条并发连接。
  • 单连接展开后的订阅项上限 500(contracts × periods 等逐项累加)。
  • 重连后须重新执行完整的 authsubscribe 流程。

连接地址

环境URL
生产环境wss://bopenapi-ws.bgwapi.io/ws
  • 路径:GET /ws(WebSocket Upgrade)
  • 可选 query 参数 ?partnerCode=... 仅用于日志,不参与鉴权
  • 测试/预发环境请联系对接团队申请。
  • 生产环境默认协商 permessage-deflate(RFC 7692)压缩 JSON 帧。主流客户端库(浏览器、ws、Python websockets)通常自动处理;自定义 WebSocket 实现须支持扩展协商。

快速接入

典型客户端流程:

connect → auth → subscribe → listen events → ping/pong → reconnect

最小消息序列:

{"op":"auth","id":"a1","params":{"apiKey":"ak_xxx","timestamp":1780480800000,"nonce":"abc123...","signature":"..."}} {"op":"subscribe","id":"s1","topic":"market.v1.ticker","params":{"chain":"bnb","contracts":["0x55d398326f99059ff775485246999027b3197955"]}}

鉴权成功响应:

{"type":"auth_ack","id":"a1","code":0,"msg":"ok","ts":1780480800001}

订阅成功响应:

{"type":"subscribe_ack","id":"s1","topic":"market.v1.ticker","subId":"server-sub-id","code":0,"msg":"ok","ts":1780480800002}

事件推送:

{"type":"event","topic":"market.v1.ticker","subId":"server-sub-id","seq":1,"ts":1780480800003,"data":{...}}

鉴权

与 HTTP 鉴权的差异: 使用相同的 API Key 和 API Secret,但签名算法与 HTTP 鉴权 完全不同。时间窗口也不同:HTTP 为 ±10 分钟,WebSocket auth 为 ±5 分钟

连接建立后首帧必须是 auth

Auth 请求

{ "op": "auth", "id": "auth-001", "params": { "apiKey": "ak_xxxxxxxxxxxxxxxxxx", "timestamp": 1780480800000, "nonce": "8f14e45fceea167a5a36dedd4bea2543", "signature": "Base64HmacSha256..." } }
字段说明
apiKey合作方 API Key
timestampUnix 毫秒时间戳,与服务端时差须在 ±5 分钟内
nonce随机串,16–64 字符,5 分钟内不可复用
signatureHMAC-SHA256 签名(见下方算法)

签名算法

function buildSignString(apiKey, nonce, timestamp) { return JSON.stringify({ apiKey, nonce, timestamp }); } function hmacSign(apiSecret, signString) { return crypto.createHmac('sha256', apiSecret).update(signString).digest('base64'); } const ts = Date.now(); const nonce = randomNonce(); // 32 位随机字符串 const signature = hmacSign(apiSecret, buildSignString(apiKey, nonce, String(ts)));

规则:

  • 签名串中 timestamp字符串(如 "1780480800000")。
  • JSON key 按字典序排列:apiKeynoncetimestamp
  • 签名 = Base64(HMAC-SHA256(apiSecret, signString))

Auth 响应

成功:

{"type":"auth_ack","id":"auth-001","code":0,"msg":"ok","ts":1780480800001}

失败(服务端发送 ack 后关闭连接):

{"type":"auth_ack","id":"auth-001","code":40101,"msg":"invalid signature","ts":1780480800001}

5 秒内未收到 auth,服务端直接断开连接,不回 ack(错误码 40801)。

协议说明

客户端请求帧

所有客户端请求均含 op 字段:

op说明
auth鉴权(首帧)
subscribe订阅
unsubscribe退订(通过 subId
ping客户端主动心跳

通用字段:

字段必填说明
op操作类型
id客户端请求 ID,用于 ack 对齐
topicsubscribe 必填WebSocket topic 名称
paramsauth / subscribe / unsubscribe各操作对应参数

认证前限制(连接建立后须先完成 auth):

操作未 auth 时行为
subscribe / unsubscribe返回 40108 ack,然后关连接
ping直接关连接,不回 ack
其他非法帧忽略

服务端响应帧

type说明
auth_ack鉴权结果
subscribe_ack订阅结果
unsubscribe_ack退订结果
event实时数据推送
ping服务端主动心跳
pong心跳响应

订阅

{ "op": "subscribe", "id": "sub-001", "topic": "market.v1.ticker", "params": { "chain": "bnb", "contracts": ["0x55d398326f99059ff775485246999027b3197955"] } }

成功响应:

{ "type": "subscribe_ack", "id": "sub-001", "topic": "market.v1.ticker", "subId": "server-sub-id", "code": 0, "msg": "ok", "ts": 1780480800002 }

说明:

  • 一次 subscribe 返回一个 subId,覆盖请求内所有 contracts/periods。
  • 相同参数重复 subscribe 返回原 subId,不新建订阅。
  • EVM 合约地址服务端规范化为小写;Solana/Tron base58 地址大小写敏感。

退订

{ "op": "unsubscribe", "id": "unsub-001", "params": { "subId": "server-sub-id" } }

成功响应:

{ "type": "unsubscribe_ack", "id": "unsub-001", "topic": "market.v1.ticker", "subId": "server-sub-id", "code": 0, "msg": "ok", "ts": 1780480800003 }

支持的 Topic

WS Topic数据类型订阅参数
market.v1.ticker链上成交chain(必填)+ contracts[](必填,至少 1 个)
market.v1.klineK 线chain + contracts[] + periods[](均必填)
market.v1.tokenToken 元数据chain + contracts[](必填)+ types[](可选,默认 new,update
rwa.v1.tickerRWA 价格变动tickers[](必填,至少 1 个)
rwa.v1.klineRWA K 线tickers[] + periods[](均必填)
rwa.v1.txRWA 链上交易tickers[](必填,至少 1 个)

market.v1.ticker

{ "op": "subscribe", "id": "sub-ticker", "topic": "market.v1.ticker", "params": { "chain": "sol", "contracts": ["4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R"] } }

market.v1.kline

{ "op": "subscribe", "id": "sub-kline", "topic": "market.v1.kline", "params": { "chain": "bnb", "contracts": ["0x55d398326f99059ff775485246999027b3197955"], "periods": ["1m", "5m"] } }

订阅参数说明:

  • chaincontracts[]periods[] 均为必填;缺省 periods 将返回 40001
  • 一次 subscribe 覆盖 contracts × periods 笛卡尔积,订阅项按此累加(计入 500 上限)。
  • 相同参数重复 subscribe 返回原 subId,不新建订阅。

支持的 period(与 HTTP K 线 API 一致):

period含义
1s1 秒
1m1 分钟
5m5 分钟
15m15 分钟
30m30 分钟
1h1 小时
4h4 小时
1d1 天
1w1 周

订阅 ack 成功仅表示参数格式合法。若某 chain + period 组合上游暂无数据源,则不会收到对应 event(不会报错)。

多合约、多周期示例(展开为 2 × 3 = 6 个订阅项):

{ "op": "subscribe", "id": "sub-kline-multi", "topic": "market.v1.kline", "params": { "chain": "sol", "contracts": [ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "So11111111111111111111111111111111111111112" ], "periods": ["1m", "5m", "1h"] } }

market.v1.token

{ "op": "subscribe", "id": "sub-token", "topic": "market.v1.token", "params": { "chain": "eth", "contracts": ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], "types": ["new", "update"] } }
  • types 缺省 — 默认为 ["new", "update"]

rwa.v1.ticker / rwa.v1.tx

{ "op": "subscribe", "id": "sub-rwa-ticker", "topic": "rwa.v1.ticker", "params": { "tickers": ["ONDO:USDY", "ONDO:OUSG"] } }

rwa.v1.kline

{ "op": "subscribe", "id": "sub-rwa-kline", "topic": "rwa.v1.kline", "params": { "tickers": ["ONDO:USDY"], "periods": ["1m", "1d"] } }

订阅参数说明:

  • tickers[]periods[] 均为必填
  • 一次 subscribe 覆盖 tickers × periods 笛卡尔积,订阅项按此累加(计入 500 上限)。
  • 上游一次可能推送最多 5 根 K 线,网关拆成多条独立 event 下发。

支持的 period: 1m5m15m1h4h12h1d1w1M

推送消息

所有推送消息共用以下 envelope:

{ "type": "event", "topic": "market.v1.ticker", "subId": "server-sub-id", "seq": 1024, "ts": 1780480800000, "data": {} }
字段说明
seq单连接内单调递增。出现 gap 表示有 event 被丢弃,可能原因:① 断线期间的事件(不会补发);② 客户端处理慢,服务端发送队列满时 drop oldest;③ 长时间慢消费触发服务端主动断连。不要依赖 seq 做完整性校验。
ts网关推送时间(Unix 毫秒)
data各 topic 的业务数据

价格、数量、金额类字段统一为 string,防止 JavaScript 精度丢失。

market.v1.ticker

字段类型说明
chainstring链标识(小写)
contractstring代币合约地址
sidestringbuysell
amountstring成交数量
pricestring成交价格
valuestring成交金额
txHashstring交易哈希
txFromstring交易发起地址
blockNumberint64区块高度
indexint64事件索引
timestampint64链上时间戳(Unix 秒)

网关仅推送已纳入 K 线统计的成交(上游 is_adopt=true)。未收录的链上成交不会通过 WebSocket 下发,这不代表链上无成交。

示例:

{ "type": "event", "topic": "market.v1.ticker", "subId": "sub-001", "seq": 1, "ts": 1780480800000, "data": { "chain": "sol", "contract": "Mint1", "side": "buy", "amount": "1250.5", "price": "1.0001", "value": "1250.63", "txHash": "Hash1", "txFrom": "From1", "blockNumber": 285, "index": 12, "timestamp": 1717382401 } }

market.v1.kline

字段类型说明
chainstring链标识
contractstring代币合约地址
periodstringK 线周期(如 1m
open / high / low / closestringOHLC 价格
buyAmount / sellAmountstring买卖数量
buyValue / sellValuestring买卖金额
txnint64成交笔数
timestampint64K 线时间戳(Unix 秒)

示例:

{ "type": "event", "topic": "market.v1.kline", "subId": "sub-002", "seq": 2, "ts": 1780480800001, "data": { "chain": "bnb", "contract": "0xabc", "period": "1m", "open": "1", "high": "2", "low": "0.8", "close": "1.5", "buyAmount": "10", "sellAmount": "5", "buyValue": "100", "sellValue": "50", "txn": 7, "timestamp": 1717382401 } }

market.v1.token

字段类型说明
chainstring链标识
contractstring代币合约地址
typestringnewupdate
updatedFieldsobject变更字段(仅 update 事件)
symbolstring代币符号
namestring代币名称
iconstring图标 URL
decimalsint精度
aboutstring简介
issueDatestring发行日期
supplyTotalstring总供应量

示例(新代币):

{ "type": "event", "topic": "market.v1.token", "subId": "sub-003", "seq": 3, "ts": 1780480800002, "data": { "chain": "eth", "contract": "0xabc", "type": "new", "symbol": "ABC", "name": "ABC Token", "icon": "https://example.com/a.png", "decimals": 18, "about": "about abc", "issueDate": "2024-01-01", "supplyTotal": "1000000" } }

rwa.v1.ticker

字段类型说明
tickerstringRWA 标的(如 ONDO:USDY
chainstring链标识
symbolstring符号
addressstring合约地址
dataSourcestring数据源
pricestring当前价格
price24hAgostring24 小时前价格
price24hChangestring24 小时价格变动
price24hChangeRatiostring24 小时变动比率
timestampint64时间戳(Unix 秒)
chainAssetsarray多链合约地址列表

示例:

{ "type": "event", "topic": "rwa.v1.ticker", "subId": "sub-004", "seq": 4, "ts": 1780480800003, "data": { "ticker": "ONDO:USDY", "chain": "eth", "symbol": "USDY", "address": "0xA0b", "dataSource": "ondo", "price": "1.0801", "price24hAgo": "1.0774", "price24hChange": "0.0027", "price24hChangeRatio": "0.0025", "timestamp": 1717382401, "chainAssets": [ {"chain": "eth", "address": "0xA0b"}, {"chain": "sol", "address": "EPjF"} ] } }

rwa.v1.kline

上游一次可能推送最多 5 根 K 线,网关拆成多条独立 event 下发。

字段类型说明
tickerstringRWA 标的
symbolstring符号
chainstring链标识
addressstring合约地址
periodstringK 线周期
dataSourcestring数据源
open / high / low / closestringOHLC 价格
buyAmount / sellAmountstring买卖数量
buyValue / sellValuestring买卖金额
txnint64成交笔数
timestampint64K 线时间戳
chainAssetsarray多链合约地址列表

rwa.v1.tx

字段类型说明
idstring事件 ID
tickerstringRWA 标的
chainstring链标识
contractstring合约地址
txHashstring交易哈希
txFromstring交易发起地址
blockNumberint64区块高度
txIndexint交易索引
eventIndexint事件索引
sidestringbuysell
amountstring成交数量
priceUsdstringUSD 价格
volumeUsdstringUSD 成交额
timestampint64时间戳(Unix 秒)

示例:

{ "type": "event", "topic": "rwa.v1.tx", "subId": "sub-005", "seq": 5, "ts": 1780480800004, "data": { "id": "evt-1", "ticker": "ONDO:USDY", "chain": "eth", "contract": "0xabc", "txHash": "0xhash", "txFrom": "0xfrom", "blockNumber": 123, "txIndex": 4, "eventIndex": 5, "side": "buy", "amount": "10.5", "priceUsd": "1.08", "volumeUsd": "11.34", "timestamp": 1717382401 } }

心跳

客户端主动 ping

请求:

{"op":"ping","id":"client-ping-001"}

响应:

{"type":"pong","id":"client-ping-001","ts":1780480800003}

服务端主动 ping

服务端空闲 20 秒后主动发送 ping:

{"type":"ping","id":"conn-1-ping-1","ts":1780480800100}

客户端须立即回复:

{"type":"pong","id":"conn-1-ping-1","ts":1780480800101}

60 秒内无任何入站消息,服务端主动断开连接。

错误码

错误通过 *_ack 帧的 code != 0 返回。

错误码含义是否关连接
0成功
40001字段非法 / topic 不存在
40101验签失败
40102apiKey 不存在或已禁用
40104timestamp 超出 ±5 分钟窗口
40105nonce 重放
40106auth 字段非法
40107连接数达上限(最多 5 条)
40108未认证
40401subId 不存在
40901订阅项超限(最多 500 项)
40801auth 超时(不回 ack)
50001partner 服务不可用
50002Redis 不可用

完整示例(Node.js)

const crypto = require('node:crypto'); const WebSocket = require('ws'); const WS_URL = 'wss://bopenapi-ws.bgwapi.io/ws'; const API_KEY = process.env.WS_API_KEY; const API_SECRET = process.env.WS_API_SECRET; function buildSignString(apiKey, nonce, timestamp) { return JSON.stringify({ apiKey, nonce, timestamp }); } function hmacSign(apiSecret, signString) { return crypto.createHmac('sha256', apiSecret).update(signString).digest('base64'); } function randomNonce() { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let out = ''; for (let i = 0; i < 32; i++) out += chars[Math.floor(Math.random() * chars.length)]; return out; } function send(ws, obj) { return new Promise((resolve, reject) => { ws.send(JSON.stringify(obj), (err) => (err ? reject(err) : resolve())); }); } class MessageRouter { constructor(ws) { this.queue = []; this.waiters = []; this.closed = false; ws.on('message', (raw) => { let msg; try { msg = JSON.parse(raw.toString()); } catch { return; } if (this.waiters.length) this.waiters.shift()(msg); else this.queue.push(msg); }); } close() { this.closed = true; while (this.waiters.length) this.waiters.shift()(null); } next() { if (this.queue.length) return Promise.resolve(this.queue.shift()); if (this.closed) return Promise.resolve(null); return new Promise((resolve) => this.waiters.push(resolve)); } } async function readAck(router, wantType, wantID, onEarly) { while (true) { const msg = await router.next(); if (!msg) throw new Error('connection closed while waiting for ack'); if (msg.type === wantType && msg.id === wantID) return msg; if (onEarly) onEarly(msg); } } function handlePing(ws, msg) { send(ws, { type: 'pong', id: msg.id, ts: Date.now() }).catch(() => {}); } async function auth(ws, router) { const id = `a-${Date.now()}`; const ts = Date.now(); const nonce = randomNonce(); const sig = hmacSign(API_SECRET, buildSignString(API_KEY, nonce, String(ts))); await send(ws, { op: 'auth', id, params: { apiKey: API_KEY, timestamp: ts, nonce, signature: sig } }); const ack = await readAck(router, 'auth_ack', id, (msg) => { if (msg.type === 'ping') handlePing(ws, msg); }); if (ack.code !== 0) throw new Error(`auth failed: ${ack.code} ${ack.msg}`); } async function subscribe(ws, router, topic, params) { const id = `s-${Date.now()}`; await send(ws, { op: 'subscribe', id, topic, params }); const ack = await readAck(router, 'subscribe_ack', id, (msg) => { if (msg.type === 'ping') handlePing(ws, msg); }); if (ack.code !== 0) throw new Error(`subscribe failed: ${ack.code} ${ack.msg}`); return ack.subId; } async function main() { const ws = new WebSocket(WS_URL); await new Promise((resolve, reject) => { ws.once('open', resolve); ws.once('error', reject); }); const router = new MessageRouter(ws); await auth(ws, router); const subId = await subscribe(ws, router, 'market.v1.ticker', { chain: 'bnb', contracts: ['0x55d398326f99059ff775485246999027b3197955'], }); console.log('subscribed:', subId); (async () => { while (true) { const msg = await router.next(); if (!msg) break; if (msg.type === 'ping') { handlePing(ws, msg); continue; } if (msg.type === 'event') { console.log(`[${msg.topic}] seq=${msg.seq}`, msg.data); } } })(); setInterval(() => { if (ws.readyState === WebSocket.OPEN) { send(ws, { op: 'ping', id: `kp-${Date.now()}` }); } }, 15_000); } main().catch(console.error);

最佳实践

  • HTTP + WebSocket 组合接入 — 启动/重连时用 HTTP 补历史,WebSocket 只负责实时增量。
  • 指数退避重连 — 初始 1 秒,最大 60 秒;每次重连执行完整 authsubscribe
  • 重连后 HTTP 补数据 — 通过 HTTP API 查询断线窗口内的缺失数据,再恢复实时流。
  • 响应服务端 ping — 收到 type: ping 后立即回 type: pongid 保持一致。
  • 不要依赖 seq 保证完整性 — 见上文 seq 字段说明(断线丢失、队列丢弃、慢消费断连);出现 gap 后须通过 HTTP 回补。
  • 监控连接活性 — 45 秒无入站消息时建议主动重连。
Last updated on