Authentication
A single key opens the three channels. It travels differently on each one (a header in REST and in MCP, a URL parameter at the handshake in WebSocket), but its value is the same everywhere, and it is the rights of your plan that apply on each.
#REST API
The key goes in the X-API-KEY header. No URL parameter, no Authorization: Bearer: a key in a query string would end up in the access logs of every intermediary along the way.
curl -H "X-API-KEY: $BYTNODE_KEY" \
"https://api.bytnode.com/v1/basis?symbol=ETHUSDT"The comparison runs in constant time, so a wrong key takes exactly as long to be rejected as an almost-right one.
#The route without a key
Only one route answers without authentication: /v1/health, which says whether the service is up. The two routes you call right after (the freshness of the data and the asset catalogue) require the key, like everything else. /v1/status is a probe: never counted against the monthly quota, but subject to the rate limit of the plan. /v1/symbols belongs to the price and volume family, open on every plan, and counts like any other call.
# The only route that answers without a key.
curl https://api.bytnode.com/v1/health
# The next two require the key. /v1/status is a probe: never counted
# against the monthly quota, but subject to the rate limit of the plan.
curl -H "X-API-KEY: $BYTNODE_KEY" https://api.bytnode.com/v1/status
curl -H "X-API-KEY: $BYTNODE_KEY" https://api.bytnode.com/v1/symbols#WebSocket stream
The key is presented in the URL, as the ?api_key= parameter, and it is checked at the handshake, before the connection opens. A browser cannot set a header on a WebSocket: this is the only form that works everywhere, and it is the one the examples use.
import asyncio
import json
import os
import websockets
async def main() -> None:
# The key travels in the URL, at the handshake: the connection opens
# already authenticated. A missing or invalid key gets 403 before it opens.
url = f"wss://api.bytnode.com/ws?api_key={os.environ['BYTNODE_KEY']}"
async with websockets.connect(url) as ws:
await ws.send(json.dumps({
"op": "subscribe",
"channels": ["trades.futures.BTCUSDT"],
}))
print(await ws.recv()) # {"op": "subscribed", "channels": [...]}
asyncio.run(main())The session opens already authenticated: the first message can be the subscription. A key that is missing, invalid or revoked, or a plan that does not include the stream, gets a 403 at the handshake and nothing is established. The plan’s ceiling on simultaneous connections applies to the whole account: the connection too many is closed with code 4003. The old frame {"op": "auth", "api_key": "…"} is still accepted and answers ok, but it is optional.
#MCP server
The MCP server expects the same key, in the same X-API-KEY header, set on the HTTP connection. Without it, no tool is exposed: the server refuses by default rather than opening by default.
from fastmcp import Client
headers = {"X-API-KEY": "your_key"}
async with Client("https://mcp.bytnode.com", headers=headers) as client:
info = await client.call_tool("get_system_info", {})The exact shape of the configuration file depends on the agent; the principle does not change: a URL and a header. See MCP server.
#What a refusal says
| Channel | Response | Cause |
|---|---|---|
| REST | 403, X-Deny-Reason: key | Header missing, key unknown or revoked. The body says « Cle API absente, invalide ou revoquee ». |
| REST | 403, X-Deny-Reason: family | The key is valid, but the family of this endpoint is not in your plan. |
| MCP | 401, header X-API-KEY requis | Header missing on the connection, or key refused. The check happens before a single tool is announced; after that, every tool call is judged against the rights of the key. |
| WebSocket | 403 at the handshake | Key missing or invalid in the URL (key), or plan without the stream (websocket). The connection does not open. |
| WebSocket | close 4003 | The plan’s ceiling on simultaneous connections reached, for the whole account. The reason gives the ceiling. |
#Protecting and rotating the key
Where to keep it
- In an environment variable or a secrets manager, never in the code repository.
- Server-side only. A key placed in page JavaScript is public, whatever the obfuscation.
- One key per environment: development, staging, production. A leak can then be revoked without stopping the rest.
Rotating it
- Create the new key in the console. Both stay valid during the overlap.
- Deploy it everywhere, then check that traffic on the old one has fallen back to zero.
- Revoke the old one. Revocation is immediate.