入门

应用与 AI 智能体的身份验证

一把密钥即可打开三个通道。它在每个通道中都放在请求头里(X-API-KEY,用于 REST、MCP 以及 WebSocket 握手);网页无法保存机密,因此改用由你的服务器签发的短期令牌打开数据流。密钥的值处处相同,且在每个通道上适用的都是你方案的权限。

#REST API

密钥放在 X-API-KEY 请求头中;只能发送 Bearer 令牌的客户端也可以放在 Authorization: Bearer 中(两者同时发送时以 X-API-KEY 为准)。切勿放在 URL 中:放在查询字符串中的密钥会出现在沿途每个中间节点的访问日志里。仅支持 HTTPS:通过明文 http:// 发出的请求会被拒绝并返回 403 https_required,而不会被重定向,因为重定向会让密钥在每次调用时以明文传输。如果你看到这个错误,请轮换它所携带的密钥。

curl
curl -H "X-API-KEY: $BYTNODE_API_KEY" \
  "https://api.bytnode.com/v1/basis?symbol=ETHUSDT"

比较以恒定时间执行:错误的密钥与几乎正确的密钥被拒绝所需的时间完全相同。

#无需密钥的接口

只有一个接口无需认证即可访问:/v1/health,用于告知服务是否正常。其他所有接口都需要密钥。有两个接口从不计入月度配额(仅受速率限制约束):/v1/health 和 /v1/limits(你的密钥的限制)。/v1/status(数据新鲜度)和 /v1/symbols(资产目录)属于价格与成交量市场数据族,在所有方案中开放,与其他调用一样计数。

GET/v1/health无需密钥完整参考
此接口的全部 1 个字段(取自接口契约)
database`connected` when the service reaches its database. A failing check answers 503 `service_unavailable` with `database` in `error.details`.
GET/v1/status完整参考
此接口的全部 15 个字段(取自接口契约)
computed_atISO 8601 UTC, milliseconds (2026-09-28T14:00:00.000Z)When the status was measured: the OLDEST of its measurements (each data family is measured on its own, cached up to 5 minutes for market feeds and 30 seconds for the others). Every `age_s` is measured at or after this instant, not at `server_time`.
state`ok`, or `degraded` when at least one feed is stale or failed its check. The envelope's `status` stays `ok` (the request succeeded).
feed_countcountPublic feeds checked.
stale_feed_countcountFeeds older than their threshold.
error_feed_countcountFeeds that could not be checked.
max_age_ssecondsAge of the oldest feed.
feedsOne row per public feed.
feeds.feedPublic feed name. The closed list of this contract version; `routes` gives the routes each one supplies.
feeds.familyData family the feed belongs to, as in `/v1/limits` and the `x-family` of each route: `price_volume`, `derivatives`, `microstructure`, `trades`, `options`, `onchain`, `macro`, `etf`, `tokenomics` or `sentiment`.
feeds.routesThe public routes (contract paths) this feed supplies: watch this feed to know whether they are fresh. A per-timeframe feed (`cvd_1h`) supplies them at that timeframe.
feeds.age_ssecondsAge of the latest row of the feed. For a feed made of several series (one per asset, per macro series, per ETF), the age of its OLDEST series. For the market-hours intraday series (`macro_intraday_<tf>`, not `_continuous`), counted on weekdays only, like their threshold. `null` if the feed is empty or could not be checked.
feeds.expected_interval_ssecondsMaximum legitimate age of the feed.
feeds.threshold_ssecondsAge above which the feed is stale (1.5 × expected_interval_s).
feeds.is_staleWhether the feed is stale: older than `threshold_s`, or (series feed) at least one of its series is. `null` when the feed could not be checked (counted in `error_feed_count`).
feeds.stale_venue_countcountVenues lagging on this feed; `null` when the feed is not split by venue (every non-market feed).
GET/v1/symbols完整参考
此接口的全部 5 个字段(取自接口契约)
symbolTrading pair, e.g. `BTCUSDT`.
base_assetBase asset of the pair.
nameHuman-readable name.
quote_assetQuote currency of the pair (`USDT` for BTCUSDT, `USDC` for USDTUSDC): the currency of its prices.
marketsMarkets served for the pair: `spot` and `futures` (USDT-margined perpetual), or `spot` only for the two stablecoin pairs (their `/futures` routes answer `unavailable: not_applicable`).
curl
# The only route that answers without a key.
curl https://api.bytnode.com/v1/health

# /v1/limits requires the key but is never counted against the quota.
curl -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/limits

# /v1/status and /v1/symbols require the key and count as one request
# of the price & volume family, like any other call.
curl -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/status
curl -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/symbols

#WebSocket 数据流

在服务器端,密钥放在握手的 X-API-KEY 请求头中,与 REST 完全相同。网页不应持有你的密钥,而浏览器也无法为 WebSocket 设置请求头:网页改用 ?token= 打开数据流,该短期令牌由你的服务器通过 POST /v1/ws/token 获取。两种方式下,凭据都在握手时校验,早于连接建立。

import asyncio
import json
import os

import websockets  # pip install websockets


async def main() -> None:
    # From a server, the key goes in the X-API-KEY header of the handshake:
    # the connection opens already authenticated. A missing or invalid key gets
    # 401 before it opens (a plan without the stream: 403).
    headers = {"X-API-KEY": os.environ["BYTNODE_API_KEY"]}
    async with websockets.connect("wss://api.bytnode.com/ws", additional_headers=headers) as ws:
        await ws.send(json.dumps({
            "op": "subscribe",
            "channels": ["trades.futures.BTCUSDT"],
        }))
        print(await ws.recv())   # {"op": "subscribed", "channels": [...]}


asyncio.run(main())

POST /v1/ws/token 由你的服务器携带密钥调用,返回 token 和一个可直接打开的 url。令牌有效期为 60 秒,且只能打开一个连接:一旦使用或过期即被拒绝,因此每次连接和每次重连都需申请新令牌。它只能打开 WebSocket,不能访问任何 REST 接口;适用的权限是握手那一刻密钥的权限(其间被吊销的密钥将无法连接)。不含数据流的方案会收到 403 plan_websocket;签发令牌不计入每月配额。

curl
# On YOUR server (the key stays there): issue a token for the browser.
curl -X POST -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/ws/token
# {"status":"ok", …, "data_type":"ws_token","data":{"token":"wst_…",
#  "expires_at":"…","expires_in_s":60,"url":"wss://api.bytnode.com/ws?token=wst_…"}}

会话在打开时即已认证:第一条消息就可以是订阅。缺少凭据时,握手返回 401 missing_api_key;密钥未知或已吊销,或令牌无效、过期或已被使用时返回 401 invalid_api_key;方案不包含数据流时返回 403 plan_websocket,连接不会建立。握手时的 503 与密钥无关:数据流服务已达容量上限,请稍后重试。方案的同时连接上限按整个账户计算:超出的连接以代码 4003 关闭。旧的 {"op": "auth", "api_key": "…"} 帧仍被接受并返回 authenticated,但它是可选的。

?token= 和 ?api_key= 参数只在 WebSocket 握手时被接受,REST 接口从不接受,且网关不会记录 /ws 的请求行。?api_key= 对现有客户端仍然有效,但放在 URL 中的密钥会进入浏览器历史和代理日志:在服务器端请使用请求头,在网页中请使用令牌。协议细节见 WebSocket 数据流页面。

#MCP 服务器

MCP 服务器需要同一把密钥,放在同一个 X-API-KEY 请求头中,设置在 HTTP 连接上。没有密钥时不会暴露任何工具:服务器默认拒绝,而不是默认开放。

import asyncio
import os

from fastmcp import Client  # pip install fastmcp
from fastmcp.client.transports import StreamableHttpTransport


async def main() -> None:
    transport = StreamableHttpTransport(
        "https://api.bytnode.com/mcp", headers={"X-API-KEY": os.environ["BYTNODE_API_KEY"]}
    )
    async with Client(transport) as client:
        info = await client.call_tool("get_system_info", {})
        print(info)


asyncio.run(main())

配置文件的具体格式取决于AI 智能体;原则不变:一个 URL 和一个请求头。见 MCP 服务器。

#拒绝响应说明了什么

通道响应原因
REST401, WWW-Authenticate缺少请求头:error.code: missing_api_key。密钥未知或已吊销:error.code: invalid_api_key。消息会说明该怎么做,例如 « API key missing: send it in the X-API-KEY header. Create a free key at https://bytnode.com/sign-up. »
REST403, X-Deny-Reason: plan_family密钥有效,但该端点所属的市场数据族不在你的方案中。
MCP401, WWW-Authenticate连接上缺少请求头,或密钥被拒绝。校验发生在公布任何工具之前;之后,每次工具调用都会按密钥的权限判断。
WebSocket握手时返回 401(密钥)或 403(方案)密钥或令牌缺失、无效、过期或已被使用(missing_api_key、invalid_api_key),或方案不含数据流(plan_websocket)。连接不会打开。
WebSocket关闭 4003 / 40054003:已达到方案的同时连接上限(按整个账户计算),关闭原因中给出上限。4005:客户端消费数据流过慢,原因为 client too slow。
明文 HTTP403, https_required使用 http:// 而不是 https:// 发起调用:被拒绝,绝不重定向。如果该请求携带了你的密钥,它已以明文传输:请轮换密钥。
WebSocket握手时返回 503数据流服务已达到容量上限(所有账户合计)。问题不在密钥,也不在方案:请稍后以递增的间隔重试。

#保护与轮换密钥

存放在哪里

  • 放在环境变量或密钥管理器中,绝不放入代码仓库。
  • 每个环境一把密钥:开发、预发布、生产。这样一旦泄露,可以只吊销它而不影响其他环境。
  • 绝不要放在网页中:前端代码里的密钥任何访问者都能读到。请从你的后端调用 REST 接口;对于数据流,请把 POST /v1/ws/token 签发的令牌交给网页。

轮换

  • 在控制台中创建新密钥。重叠期内两把密钥均有效。
  • 将新密钥部署到所有地方,然后确认旧密钥上的流量已降为零。
  • 吊销旧密钥。吊销立即生效。