2026 年 Bybit V5 API 鉴权实操:HMAC 签名、权限分级与常见错误码排查
Bybit V5 文档把签名算法写清楚了,但没告诉你新手第一周一定卡在 10004 上。这篇拆开 V5 鉴权全流程:HMAC 签名 5 步走、API key 权限矩阵、错误码地图、recv_window 时间漂移。
上面是文章摘要,下面进入正文深读。可以配合目录逐段阅读,不会丢掉上下文。
上一个 sprint 我给一个 basis trade 策略接 funding rate 监控,篮子里只有 Bybit V5 让我前六个请求全吃了 retCode: 10004。API key 没错,secret 没错,端点是文档里指明的那一个,响应还是说签名不对。Bybit V5 官方文档把签名规范写清楚了——HMAC 进去什么字节、用哪些 header——但是没告诉你新手第一周一定要花掉的那半天,全在排错误码上面。这篇就是我当时希望存在的那篇连接文。
如果你是从 "bybit api docs" 或者 "bybit api documentation" 之类的搜索结果跳进来的,你要找的大概率是这一层,而不是又一份 Bybit API 端点清单。下面先用 Python 把 V5 签名一步步搭出来,然后讲清楚每个 API key 权限到底解锁了什么,最后给一份你可以一直开着的错误码地图。
V5 改了什么,你的 V3 代码为什么悄悄坏了
Bybit 在 2023 年初上线了 V5 统一接口,之后一年陆续把 Spot、Linear、Inverse、Option 的 V3 surface 都标了废弃。到 2026 年,V3 对新账户已经基本下线,以前能跑的搬运代码会在两个地方安静地坏掉。
第一是 prehash 的拼法。V3 是把 query 参数按字母排序后拼成签名串。V5 不排序。prehash 的形态是:
timestamp + api_key + recv_window + (queryString | rawBody)
GET 请求第四段是 URL 里参数实际出现的那个顺序的 query string。POST 请求第四段是 JSON body 的原始字节,发出去什么就签什么。如果 HTTP 客户端在你签完之后又重排了 JSON key,服务端看到的 body 和你签的就不一样了,签名直接挂。
第二是 header 集。V3 允许你把 api_key、timestamp、sign 这些字段塞进 URL 或者 body。V5 把所有鉴权元数据全部挪到 header——X-BAPI-API-KEY、X-BAPI-TIMESTAMP、X-BAPI-RECV-WINDOW、X-BAPI-SIGN,再加一个 X-BAPI-SIGN-TYPE: 2 表示 HMAC SHA256。如果旧 wrapper 里还有把 api_key 塞进 query string 的逻辑,V5 端点要么忽略要么直接拒绝,看具体接口。
| 字段 | V3 | V5 |
|---|---|---|
| prehash 拼接 | 参数按字母排序 | timestamp + api_key + recv_window + payload,按这个顺序 |
| POST body 签名 | 排序后的 JSON | 原始 JSON body,字节级一致 |
| 鉴权位置 | URL/body | 只走 header |
| 签名 header | sign query 或 body 字段 |
X-BAPI-SIGN |
| 签名类型 | 隐式 | X-BAPI-SIGN-TYPE: 2(HMAC)或 1(RSA) |
迁移最干净的做法是把 V3 签名 helper 整个扔掉,V5 从头写。二十行 Python 比改一千行围绕字母排序设计的老 wrapper 省事得多。
五步搭出 V5 签名
下面是完整可跑的签名函数,覆盖 GET 和 POST。不用 SDK,只要 requests 和 hmac。
import hmac
import hashlib
import json
import time
import requests
API_KEY = "YOUR_KEY"
API_SECRET = "YOUR_SECRET"
BASE_URL = "https://api.bybit.com"
RECV_WINDOW = "5000"
def sign(payload: str, timestamp: str) -> str:
prehash = timestamp + API_KEY + RECV_WINDOW + payload
return hmac.new(
API_SECRET.encode("utf-8"),
prehash.encode("utf-8"),
hashlib.sha256,
).hexdigest()
def signed_get(path: str, params: dict) -> dict:
# URL 里的参数顺序必须和 prehash 里一致
query_string = "&".join(f"{k}={v}" for k, v in params.items())
timestamp = str(int(time.time() * 1000))
signature = sign(query_string, timestamp)
headers = {
"X-BAPI-API-KEY": API_KEY,
"X-BAPI-TIMESTAMP": timestamp,
"X-BAPI-RECV-WINDOW": RECV_WINDOW,
"X-BAPI-SIGN": signature,
"X-BAPI-SIGN-TYPE": "2",
}
url = f"{BASE_URL}{path}?{query_string}"
return requests.get(url, headers=headers).json()
def signed_post(path: str, body: dict) -> dict:
# 关键:只序列化一次,签那一份字节,发那一份字节
body_str = json.dumps(body, separators=(",", ":"))
timestamp = str(int(time.time() * 1000))
signature = sign(body_str, timestamp)
headers = {
"X-BAPI-API-KEY": API_KEY,
"X-BAPI-TIMESTAMP": timestamp,
"X-BAPI-RECV-WINDOW": RECV_WINDOW,
"X-BAPI-SIGN": signature,
"X-BAPI-SIGN-TYPE": "2",
"Content-Type": "application/json",
}
return requests.post(f"{BASE_URL}{path}", headers=headers, data=body_str).json()
这段代码按顺序做了五件事:
- 拿毫秒时间戳。 Bybit 要的是 Unix epoch 毫秒,不是秒。
int(time.time() * 1000)是对的,int(time.time())一发就 10002。 - 拼 payload。 GET 用 URL 的 query string;POST 用紧凑形态的 JSON body 字符串。注意
separators=(",", ":")——json.dumps默认会在分隔符后面塞空格,那些空格是要被签进去的字节。 - 拼 prehash。
timestamp + api_key + recv_window + payload,段与段之间不加任何分隔符,也不要按字母排序。 - HMAC-SHA256 用 secret 加密。 输出 hex digest,不是 base64,V5 规范明确写了 hex。
- 发出去的 body 字节要和签的一致。 POST 这里务必用
data=body_str而不是json=body,后者会重新序列化,发出去的字节和你签的不是一份了。
第一次贴上去跑不通,十次有八次卡在第 5 步的 POST 坑。requests.post(..., json=body) 会美化 JSON 加空格,签的字节和发的字节就对不上了。永远签上面那份 body_str,再把同一份 body_str 放上线。
API key 权限分级在实战中怎么勾
Bybit API Management 页面给的是一棵勾选树,但分组和端点不是简单一一映射。下面这张表是每个权限实际解锁的能力,以及实战机器人通常勾哪几个组合。
| 权限组 | 解锁内容 | 典型机器人 |
|---|---|---|
| Read-Only | 所有 GET 端点:行情、账户状态、仓位、订单、成交 |
只读 dashboard、资金费率监控 |
| Unified Trading → Spot | 现货下单/撤单/改单,现货杠杆 | 现货网格、DCA |
| Unified Trading → Derivatives | Linear 和 Inverse 永续,杠杆和保证金模式 | 永续做市、basis trade |
| Unified Trading → Options | 期权下单、多腿组合 | 期权卖方/波动率策略 |
| Asset Transfer | 钱包内转账、主子账号转账、小额币种 convert | 资金调度、子账号路由 |
| Earn | 申购/赎回活期和定期 Earn 产品 | 闲置资金扫地僧 |
| Affiliate | Affiliate 专属上报端点 | 邀请佣金 dashboard |
| Copy Trading | 带单和跟单管理 | 跟单调度 |
| Withdraw | 提现到白名单地址 | 自动提现脚本(小心用) |
UI 上看不出来的两个实战点:
第一,Read-Only 不是交易权限的超集,在那些需要读+写上下文的端点上要单独勾。 Derivatives 的下单端点会隐式读你的仓位做保证金校验,但你不能用一个 Read-Only key 也去下单——这俩是独立授权。机器人如果既读仓位又下单,Read-Only 和对应交易权限都要勾上。
第二,Withdraw 是唯一 Bybit 强制绑 IP 白名单的权限,而且提现 key 只能往账户设置里已经加白的地址打钱。如果创建 key 的时候忘了填 IP,这个开关是灰的。IP 要先想好再建 key,否则要重建。
你真正需要的错误码地图
V5 错误码总量上千,加上端点特化的(合约找不到、仓位超限之类)能更多。鉴权层这一面,新手第一周会遇到的主要在 100xx 段。下面这张是实战 triage 表:
| 错误码 | 含义 | 第一步排查 |
|---|---|---|
| 10001 | 参数错误 | 必填字段漏了或者类型错了。把发出去的 params 打印出来,对照端点 schema |
| 10002 | 请求时间戳无效,请检查 timestamp / recv_window | 服务端时间减你的时间戳超过 recv_window。NTP 对时,或者把 X-BAPI-RECV-WINDOW 调到 10000+ |
| 10003 | API key 无效 | key 字符串错了,或者在 UI 上被删了,或者你用 testnet key 打了 mainnet(反之亦然) |
| 10004 | 签名错误 | 签名对不上。回上面 V5 prehash 那节,POST body 那个坑是最常踩的 |
| 10005 | 当前 API key 权限不足 | 端点要的权限你 key 没勾上。重看权限矩阵,必要时重建 key |
| 10006 | 访问频率超限 | 限流触发了。退避一下,看看这个数据是不是该用 WebSocket 拉 |
| 10010 | IP 不匹配 | 请求来源 IP 不在 key 的白名单里。机器人换机房之后特别容易撞这个 |
| 10016 | 服务端错误 | Bybit 那边的问题,指数退避重试就好,别先怀疑自己 |
| 10018 | IP 限流超限 | 和 10006 不一样,这个是每 IP 的限额。分 IP 或者降速 |
任何 100xx 触发,建议的排查顺序:
- 同样的 payload 字节,本机用 curl 重打一遍。本机能过、VPS 过不去,问题在 10010 或者跨区时间漂移。
- 在代码里把 prehash 字符串和签名打出来。
retCode是 10004 但 prehash 看上去对,那是 POST body 在线上被改了字节。 - 在 bot 主机上请求
GET /v5/market/time(这个端点无需鉴权),结果减本地时钟。漂移超过 1 秒,行情活跃那几分钟 10002 一定会来。
recv_window 和跨区时间漂移
recv_window 的判定是地理分布式机器人最容易翻车的地方。默认 5000 毫秒看着宽,直到你的 VPS 在圣保罗、Bybit 接入点解析到东京节点,美盘 funding flip 那几分钟 RTT 偶尔顶到 4 秒——这时候请求就开始静默失败。
生产环境两种打法可行:
- 宽窗口、低信任路径。
X-BAPI-RECV-WINDOW拉到 15000-20000,接受略弱的 replay 防护。读端点或者非关键下单可以这么做。 - NTP 严格对时、窄窗口。 跑
chrony或systemd-timesyncd接近源时间池,recv_window 保持 5000,接受偶尔网络抖动产生的 10002。
错误的打法是 SDK 默认 5000 完全不管。Bybit 直接拒,从用户视角看,机器人就在最关键的几分钟停止下单,外面看不出错。
如果想要一行偏执防御,加一个"分钟级"对时——每分钟调一次 GET /v5/market/time 维护一个 offset 变量,所有出站时间戳都减/加这个 offset。这是每个生产级 Bybit wrapper 都在做的事,分钟一次的限流成本几乎可以忽略。
Testnet、Mainnet、Demo Trading 是三套环境
值得单独说一下,因为每个团队都会在某个时刻搞混一次:
- Mainnet —
https://api.bybit.com。真钱真盘口。 - Testnet —
https://api-testnet.bybit.com。完全独立的用户数据库,独立 API key,有 faucet 发测试资金,盘口和真实市场不像。签名调试和端到端测试用它,策略验真别用。 - Demo Trading —
https://api-demo.bybit.com。从主网账户开出来,跑在真实市场数据上但是用虚拟余额。策略验真用它,能拿到真实深度、真实 funding 周期、真实波动率。
新手最常见的错是先在 testnet 注册熟悉、上线时忘了 testnet key 在 mainnet 完全不认。两个数据库不互通。上线前在 mainnet 新建一把 key 是标准动作。
接下来怎么用这篇
打开 Bybit API 目录页拿端点清单,把上面那段 Python 签名函数贴进项目,第一个请求打 GET /v5/account/wallet-balance?accountType=UNIFIED 看能不能拿到真实余额。能过的话,V5 后面所有端点的形态都一样——鉴权层就这一份,剩下的全是端点 schema。如果 wallet-balance 通了但其他端点还在 10004,下一个嫌疑犯永远是 POST body 序列化那个坑,从那查起。
快速跳到对应段落
下一步
读完后可以继续回到工具目录,对比具体产品。
去看工具