Primeros pasos

Autenticación de apps y agentes de IA

Una sola clave abre los tres canales. Viaja en una cabecera en cada uno (X-API-KEY, en REST, en MCP y en el handshake de WebSocket); una página web, que no puede guardar secretos, abre el flujo con un token de corta duración que le entrega tu servidor. Su valor es el mismo en todas partes, y en cada uno se aplican los permisos de tu plan.

#API REST

La clave va en la cabecera X-API-KEY, o en Authorization: Bearer para un cliente que solo sabe enviar un token Bearer (si se envían ambas, gana X-API-KEY). Nunca en la URL: una clave en una query string acabaría en los registros de acceso de cada intermediario del camino. Solo HTTPS: una petición en http:// sin cifrar se rechaza con 403 https_required, nunca se redirige, porque una redirección dejaría viajar la clave sin cifrar en cada llamada. Si ves ese error, rota la clave que llevaba.

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

La comparación se ejecuta en tiempo constante, así que una clave errónea tarda exactamente lo mismo en rechazarse que una casi correcta.

#La ruta sin clave

Solo una ruta responde sin autenticación: /v1/health, que indica si el servicio está en marcha. Todo lo demás exige la clave. Dos rutas nunca se descuentan de la cuota mensual (solo se aplica el límite de velocidad): /v1/health y /v1/limits, los límites de tu clave. /v1/status (la frescura de los datos) y /v1/symbols (el catálogo de activos) pertenecen a la familia precio y volumen, abierta en todos los planes, y cuentan como cualquier otra llamada.

GET/v1/healthsin claveReferencia completa
Los 1 campos de esta ruta (según el contrato)
database`connected` when the service reaches its database. A failing check answers 503 `service_unavailable` with `database` in `error.details`.
GET/v1/statusReferencia completa
Los 15 campos de esta ruta (según el contrato)
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/symbolsReferencia completa
Los 5 campos de esta ruta (según el contrato)
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

#Flujo WebSocket

Desde un servidor, la clave va en la cabecera X-API-KEY del handshake, exactamente como en REST. Una página web no debe tener tu clave, y un navegador no puede poner cabeceras en un WebSocket: la página abre el flujo con ?token=, un token de corta duración que tu servidor obtiene de POST /v1/ws/token. En ambos casos, la credencial se comprueba en el handshake, antes de que se abra la conexión.

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, llamada por tu servidor con su clave, devuelve un token y una url lista para abrir. Un token vale 60 segundos y abre UNA conexión: usado o caducado, se rechaza; pide uno nuevo para cada conexión y cada reconexión. Solo abre el WebSocket, nunca una ruta REST, y los permisos aplicados son los de la clave en el momento del handshake (una clave revocada entretanto ya no se conecta). Un plan sin el flujo recibe 403 plan_websocket; emitir un token no se descuenta de la cuota mensual.

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_…"}}

La sesión se abre ya autenticada: el primer mensaje puede ser la suscripción. Una credencial ausente recibe un 401 missing_api_key en el handshake; una clave desconocida o revocada, o un token inválido, caducado o ya usado, un 401 invalid_api_key; un plan sin el flujo un 403 plan_websocket; y no se establece nada. Un 503 en el handshake no tiene que ver con la clave: el servicio de flujo ha alcanzado su capacidad y hay que reintentar un poco más tarde. El límite de conexiones simultáneas del plan vale para toda la cuenta: la conexión de más se cierra con el código 4003. La antigua trama {"op": "auth", "api_key": "…"} sigue aceptándose y responde authenticated, pero es opcional.

Los parámetros ?token= y ?api_key= solo se aceptan en el handshake de WebSocket, nunca en las rutas REST, y la pasarela no registra la línea de petición de /ws. ?api_key= sigue funcionando para los clientes existentes, pero una clave en una URL acaba en el historial del navegador y en los registros de los proxies: usa la cabecera desde un servidor y el token en una página. Los detalles del protocolo están en la página Flujo WebSocket.

#Servidor MCP

El servidor MCP espera la misma clave, en la misma cabecera X-API-KEY, puesta en la conexión HTTP. Sin ella no se expone ninguna herramienta: el servidor rechaza por defecto en lugar de abrir por defecto.

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())

La forma exacta del archivo de configuración depende del agente de IA; el principio no cambia: una URL y una cabecera. Ver Servidor MCP.

#Lo que dice un rechazo

CanalRespuestaCausa
REST401, WWW-AuthenticateCabecera ausente: error.code: missing_api_key. Clave desconocida o revocada: error.code: invalid_api_key. El mensaje dice qué hacer, por ejemplo « 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_familyLa clave es válida, pero la familia de este endpoint no está en tu plan.
MCP401, WWW-AuthenticateCabecera ausente en la conexión, o clave rechazada. La comprobación ocurre antes de anunciar una sola herramienta; después, cada llamada de herramienta se evalúa según los permisos de la clave.
WebSocket401 (clave) o 403 (plan) en el handshakeClave o token ausente, inválido, caducado o ya usado (missing_api_key, invalid_api_key), o plan sin el flujo (plan_websocket). La conexión no se abre.
WebSocketcierre 4003 / 40054003: se alcanzó el límite de conexiones simultáneas del plan, para toda la cuenta, y el motivo indica el límite. 4005: un cliente demasiado lento para consumir el flujo, motivo client too slow.
HTTP sin cifrar403, https_requiredUna llamada a http:// en lugar de https://: rechazada, nunca redirigida. Si la petición llevaba tu clave, viajó sin cifrar: rótala.
WebSocket503 en el handshakeEl servicio de flujo ha alcanzado su capacidad, sumando todas las cuentas. Ni la clave ni el plan tienen la culpa: reinténtalo un poco más tarde, con un plazo creciente.

#Proteger y rotar la clave

Dónde guardarla

  • En una variable de entorno o un gestor de secretos, nunca en el repositorio de código.
  • Una clave por entorno: desarrollo, preproducción, producción. Así una filtración puede revocarse sin detener el resto.
  • Nunca en una página web: una clave en el código del navegador la puede leer cualquier visitante. Llama a las rutas REST desde tu servidor; para el flujo, entrega a la página un token de POST /v1/ws/token.

Rotarla

  • Crea la nueva clave en la consola. Ambas siguen siendo válidas durante el solapamiento.
  • Despliégala en todas partes y comprueba que el tráfico de la antigua ha caído a cero.
  • Revoca la antigua. La revocación es inmediata.