Premiers pas

Authentification des apps et agents IA

Une seule clé ouvre les trois canaux. Elle voyage dans un en-tête sur chacun (X-API-KEY, en REST, en MCP et à la poignée de main WebSocket) ; une page web, qui ne peut pas garder de secret, ouvre le flux avec un jeton éphémère que votre serveur lui délivre. Sa valeur est la même partout, et ce sont les droits de votre offre qui s’appliquent sur chacun.

#API REST

La clé va dans l’en-tête X-API-KEY, ou dans Authorization: Bearer pour un client qui ne sait poser qu’un jeton Bearer (si les deux sont envoyés, X-API-KEY l’emporte). Jamais dans l’URL : une clé dans une chaîne de requête finirait dans les journaux d’accès de chaque intermédiaire sur le chemin. HTTPS uniquement : une requête en http:// clair est refusée en 403 https_required, jamais redirigée, car une redirection laisserait la clé voyager en clair à chaque appel. Si vous voyez cette erreur, remplacez la clé qu’elle portait.

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

La comparaison s’exécute en temps constant : une clé fausse met exactement autant de temps à être rejetée qu’une clé presque juste.

#La route sans clé

Une seule route répond sans authentification : /v1/health, qui dit si le service est en marche. Tout le reste exige la clé. Deux routes ne sont jamais décomptées du quota mensuel (seule la limite de débit s’applique) : /v1/health et /v1/limits, les limites de votre clé. /v1/status (la fraîcheur des données) et /v1/symbols (le catalogue des actifs) appartiennent à la famille prix et volume, ouverte sur toutes les offres, et comptent comme n’importe quel autre appel.

GET/v1/healthsans cléRéférence complète
Les 1 champs de cette route (d’après le contrat)
database`connected` when the service reaches its database. A failing check answers 503 `service_unavailable` with `database` in `error.details`.
Les 15 champs de cette route (d’après le contrat)
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/symbolsRéférence complète
Les 5 champs de cette route (d’après le contrat)
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

#Flux WebSocket

Depuis un serveur, la clé va dans l’en-tête X-API-KEY de la poignée de main, exactement comme en REST. Une page web ne doit pas détenir votre clé, et un navigateur ne peut pas poser d’en-tête sur un WebSocket : la page ouvre le flux avec ?token=, un jeton éphémère que votre serveur obtient de POST /v1/ws/token. Dans les deux cas, l’identifiant est vérifié à la poignée de main, avant que la connexion ne s’ouvre.

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, appelée par votre serveur avec sa clé, renvoie un token et une url prête à ouvrir. Un jeton vaut 60 secondes et ouvre UNE connexion : utilisé ou expiré, il est refusé ; demandez-en un neuf pour chaque connexion et chaque reconnexion. Il n’ouvre que le WebSocket, jamais une route REST, et les droits appliqués sont ceux de la clé au moment de la poignée de main (une clé révoquée entre-temps ne se connecte plus). Une offre sans le flux reçoit 403 plan_websocket ; émettre un jeton n’est pas décompté du quota mensuel.

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 session s’ouvre déjà authentifiée : le premier message peut être l’abonnement. Un identifiant absent reçoit un 401 missing_api_key à la poignée de main ; une clé inconnue ou révoquée, ou un jeton invalide, expiré ou déjà utilisé, un 401 invalid_api_key ; une offre sans le flux un 403 plan_websocket ; et rien n’est établi. Un 503 à la poignée de main n’a rien à voir avec la clé : le service de flux a atteint sa capacité, il faut réessayer un peu plus tard. Le plafond de connexions simultanées de l’offre vaut pour tout le compte : la connexion de trop est fermée avec le code 4003. L’ancienne trame {"op": "auth", "api_key": "…"} reste acceptée et répond authenticated, mais elle est facultative.

Les paramètres ?token= et ?api_key= ne sont acceptés que sur la poignée de main WebSocket, jamais sur les routes REST, et la passerelle ne journalise pas la ligne de requête de /ws. ?api_key= fonctionne toujours pour les clients existants, mais une clé dans une URL finit dans l’historique du navigateur et les journaux des proxies : préférez l’en-tête depuis un serveur, le jeton dans une page. Le détail du protocole est sur la page Flux WebSocket.

#Serveur MCP

Le serveur MCP attend la même clé, dans le même en-tête X-API-KEY, posé sur la connexion HTTP. Sans elle, aucun outil n’est exposé : le serveur refuse par défaut plutôt que d’ouvrir par défaut.

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 forme exacte du fichier de configuration dépend de l’agent IA ; le principe ne change pas : une URL et un en-tête. Voir Serveur MCP.

#Ce que dit un refus

CanalRéponseCause
REST401, WWW-AuthenticateEn-tête absent : error.code: missing_api_key. Clé inconnue ou révoquée : error.code: invalid_api_key. Le message dit quoi faire, par exemple « 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 clé est valide, mais la famille de cet endpoint n’est pas dans votre offre.
MCP401, WWW-AuthenticateEn-tête absent sur la connexion, ou clé refusée. La vérification a lieu avant qu’un seul outil ne soit annoncé ; ensuite, chaque appel d’outil est jugé selon les droits de la clé.
WebSocket401 (clé) ou 403 (offre) à la poignée de mainClé ou jeton absent, invalide, expiré ou déjà utilisé (missing_api_key, invalid_api_key), ou offre sans le flux (plan_websocket). La connexion ne s’ouvre pas.
WebSocketfermeture 4003 / 40054003 : le plafond de connexions simultanées de l’offre est atteint, pour tout le compte, et la raison donne le plafond. 4005 : un client trop lent pour consommer le flux, raison client too slow.
HTTP en clair403, https_requiredUn appel en http:// au lieu de https:// : refusé, jamais redirigé. Si la requête portait votre clé, elle a voyagé en clair : remplacez-la.
WebSocket503 à la poignée de mainLe service de flux a atteint sa capacité, tous comptes confondus. Ni la clé ni l’offre ne sont en cause : réessayez un peu plus tard, avec un délai croissant.

#Protéger et renouveler la clé

Où la garder

  • Dans une variable d’environnement ou un gestionnaire de secrets, jamais dans le dépôt de code.
  • Une clé par environnement : développement, préproduction, production. Une fuite peut alors être révoquée sans arrêter le reste.
  • Jamais dans une page web : une clé dans le code du navigateur est lisible par chaque visiteur. Appelez les routes REST depuis votre serveur ; pour le flux, donnez à la page un jeton de POST /v1/ws/token.

La renouveler

  • Créez la nouvelle clé dans la console. Les deux restent valides pendant le recouvrement.
  • Déployez-la partout, puis vérifiez que le trafic sur l’ancienne est retombé à zéro.
  • Révoquez l’ancienne. La révocation est immédiate.