Codes d’erreur
Une erreur dit toujours ce qui n’allait pas et, quand c’est possible, ce qu’il aurait fallu écrire à la place. Cette page liste les cas, canal par canal, et sépare ce qui mérite une nouvelle tentative de ce qui n’en mérite aucune.
Un agent IA connecté par MCP reçoit ces mêmes codes : un appel d’outil en échec commence par Error (HTTP NNN) et se termine par le code stable, ce qui permet à l’agent IA de corriger son propre appel. Voir le serveur MCP plus bas.
#La forme d’une erreur
Toute erreur a une seule forme, quelle que soit la couche qui refuse (passerelle, service ou validation) : status: "error", server_time et un objet error avec un code stable, un message en anglais et, quand un paramètre est en cause, param. error.details, un objet présent quand l’erreur porte des données structurées, donne les mêmes choses pour le code : suggestion, accepted, la fenêtre de votre offre sur un history_depth, retry_after_s sur les refus temporels. Rien n’est posé à côté d’error.
{
"status": "error",
"server_time": "2026-09-30T16:07:27.808Z",
"error": {
"code": "invalid_symbol",
"message": "Unknown symbol 'BTCUSD'. Did you mean 'BTCUSDT'? Accepted values: ADAUSDT, AVAXUSDT, BNBUSDT, BTCUSDT, DOGEUSDT, ETHUSDT, GRAMUSDT, HBARUSDT, HYPEUSDT, LINKUSDT, LTCUSDT, NEARUSDT, SOLUSDT, SUIUSDT, TAOUSDT, TRXUSDT, UNIUSDT, USDCUSDT, USDTUSDC, XLMUSDT, XMRUSDT, XRPUSDT.",
"param": "symbol",
"details": {
"suggestion": "BTCUSDT",
"accepted": [
"ADAUSDT",
"AVAXUSDT",
"BNBUSDT",
"BTCUSDT",
"DOGEUSDT",
"ETHUSDT",
"GRAMUSDT",
"HBARUSDT",
"HYPEUSDT",
"LINKUSDT",
"LTCUSDT",
"NEARUSDT",
"SOLUSDT",
"SUIUSDT",
"TAOUSDT",
"TRXUSDT",
"UNIUSDT",
"USDCUSDT",
"USDTUSDC",
"XLMUSDT",
"XMRUSDT",
"XRPUSDT"
]
}
}
}Les refus liés à l’offre ont la même forme, plus error.details.retry_after_s sur les refus temporels (aussi présent dans l’en-tête Retry-After). L’en-tête X-Deny-Reason nomme la raison : rate_limited, quota_exceeded, plan_family, plan_websocket, missing_api_key, invalid_api_key ou history_depth.
{
"status": "error",
"server_time": "2026-09-30T16:07:27.808Z",
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded: your plan allows a limited number of requests per minute. Retry after the Retry-After delay.",
"details": {
"retry_after_s": 2
}
}
}Le 403 de profondeur porte X-Deny-Reason: history_depth et error.code: history_depth, comme les autres refus liés à l’offre ; son message donne la limit maximale utilisable sur cette unité de temps. Les fenêtres par offre sont sur la page Limites d’usage.
Les codes d’erreur
Testez error.code, jamais le texte du message : le code est un contrat, le message peut être reformulé. Ajouter un code n’est pas un changement cassant ; en renommer ou en supprimer un l’est.
| error.code | Cause |
|---|---|
depth_too_large | La profondeur d’un champ du snapshot dépasse son maximum documenté. |
edge_rate_limited | Trop de requêtes depuis cette adresse IP à la passerelle (anti-flood : 20 par seconde et par adresse, rafales de 40). Réessayez après 1 seconde. |
history_depth | L’historique demandé remonte plus loin que ne l’autorise votre offre. |
https_required | Plain HTTP is refused: call the same URL over https://. A key sent over http:// travelled unencrypted: rotate it. |
insufficient_data | Pas assez d’historique pour calculer l’indicateur demandé. |
internal_error | Erreur serveur inattendue. Un nouvel essai avec un délai croissant peut réussir ; si elle persiste, signalez-la avec l’en-tête X-Request-Id de la réponse. |
invalid_api_key | La clé d’API est absente, invalide ou révoquée. |
invalid_asset | L’actif n’est pas servi par cette famille. L’erreur liste les valeurs acceptées. |
invalid_channel | WebSocket : le nom du canal est mal formé ou inconnu ; l’erreur liste les formes acceptées. |
invalid_depth | La profondeur d’un champ du snapshot n’est pas un entier positif. |
invalid_indicator | Le corps de POST /v1/indicators est invalide (id, type ou paramètres). |
invalid_message | WebSocket : le message n’est pas un objet JSON lisible. |
invalid_parameter | Un paramètre a un type ou une valeur invalide. |
invalid_series | La série macro n’existe pas. Voir le catalogue /series de la famille. |
invalid_symbol | Le symbole n’est pas servi. L’erreur liste les valeurs acceptées (voir /v1/symbols). |
invalid_time_range | since_ms / until_ms ne sont pas des millisecondes epoch valides, ou since_ms > until_ms (until_ms est inclusif), ou since_ms >= before_ms. |
invalid_timeframe | L’unité de temps (timeframe) n’est pas acceptée par cette route. |
method_not_allowed | La route existe, mais pas avec cette méthode HTTP. |
missing_api_key | No API key was sent: put it in the X-API-KEY header (or api_key in the WebSocket URL). |
missing_parameter | Un paramètre obligatoire est absent. |
not_applicable | WebSocket : le canal n’a pas de sens pour cet actif (un canal futures d’un stablecoin). L’API REST répond à une telle requête par 200 et unavailable: not_applicable. |
not_authenticated | WebSocket : un message autre que auth est arrivé sur une session pas encore authentifiée (rare : la clé est vérifiée à la poignée de main, où une clé absente répond 401 invalid_api_key). |
not_found | Route inconnue. |
plan_family | Votre offre n’inclut pas cette famille de données de marché : le message la nomme, et GET /v1/limits liste les familles de votre clé. |
plan_websocket | Votre offre n’inclut pas le flux WebSocket. |
quota_exceeded | Le quota mensuel de votre offre est épuisé. Retry-After (et error.details.retry_after_s) donnent les secondes jusqu’à sa remise à zéro, le premier jour du mois (UTC). |
rate_limited | Le débit par minute de votre offre est dépassé. Réessayez après Retry-After secondes. |
repeated_parameter | Un paramètre apparaît plus d’une fois. |
service_unavailable | Indisponibilité temporaire. Réessayez après Retry-After secondes. |
timeframe_not_served | La série existe mais ne sert pas cette unité de temps (une série journalière demandée en 1h). Une unité de temps que la route ne sert jamais est invalid_timeframe. |
unknown_field | Un nom de champ de ?fields= ou du snapshot n’existe pas. |
unknown_parameter | La route ne déclare pas ce paramètre. |
upstream_timeout | La requête a pris trop de temps. Réduisez la fenêtre ou la profondeur, puis réessayez. |
const response = await fetch(url, { headers: { "X-API-KEY": key } });
const body = await response.json();
if (body.status === "error") {
switch (body.error.code) {
case "rate_limited":
case "service_unavailable":
// Transient: wait Retry-After seconds, then retry.
break;
case "history_depth":
// Ask for less history, or upgrade: error.message gives the max limit.
break;
case "invalid_symbol":
case "unknown_parameter":
// Fix the request: error.param names the culprit, error.message the fix.
break;
default:
throw new Error(body.error.code + ": " + body.error.message);
}
}#Codes HTTP
| Code | Cause | Correctif |
|---|---|---|
401Clé d’API refusée | La clé d’API est absente, invalide ou révoquée (error.code : invalid_api_key). La réponse porte WWW-Authenticate: ApiKey header="X-API-KEY". | Vérifiez que l’en-tête X-API-KEY est bien envoyé : certains clients HTTP le perdent après une redirection. Créez ou copiez une clé depuis la console. |
403Refusé par l’offre | La clé est valide mais l’offre ne couvre pas la requête ; l’en-tête X-Deny-Reason porte le même code que error.code : plan_family (famille de données de marché hors offre), plan_websocket (flux WebSocket hors offre) ou history_depth (une profondeur d’historique au-delà de la fenêtre de l’offre). | Sur plan_family et plan_websocket, la donnée existe mais n’est pas dans votre offre. Sur history_depth, error.message donne la limite maximale à utiliser. |
404Route inconnue | Le chemin n’existe pas (error.code : not_found). Le message propose la route la plus proche quand il y en a une. | Vérifiez le chemin : le message donne la route la plus proche quand il y en a une ; toutes les routes sont dans le contrat. |
405Méthode non autorisée | La route existe, mais pas avec cette méthode (error.code : method_not_allowed) ; l’en-tête Allow liste celles qu’elle accepte. | Utilisez GET (POST seulement pour /v1/indicators) ; HEAD est accepté sur toute route GET, et OPTIONS (le préflight CORS) sur toute route. |
422Paramètre manquant, inconnu ou invalide | Un symbole, un actif ou une série inconnus ou désactivés, une profondeur au-delà du plafond du champ, un paramètre d’indicateur hors bornes, symbol manquant, unité de temps hors liste, champ multi-unités demandé sans le suffixe @tf, champ inconnu dans ?fields=, paramètre que la route ne déclare pas ou paramètre répété, since_ms ou until_ms exprimé en secondes ou hors plage, corps JSON invalide. | Sur un champ ou un paramètre inconnu, la réponse suggère le nom le plus proche. Sur une unité de temps, elle liste les valeurs acceptées. |
429Débit ou quota dépassé | Soit plus de requêtes par minute que votre offre ne l’autorise, ou une rafale trop dense (X-Deny-Reason: rate_limited, Retry-After : l’attente du prochain jeton) ; soit le quota mensuel du compte est épuisé (X-Deny-Reason: quota_exceeded, Retry-After : les secondes jusqu’à sa remise à zéro). | Sur rate_limited, attendez les secondes indiquées par Retry-After. Sur quota_exceeded, le compteur repart le premier jour du mois (UTC) ; vous pouvez aussi changer d’offre depuis la console. |
503Indisponibilité momentanée | Une dépendance de lecture redémarre. À la poignée de main WebSocket, le service de flux a atteint sa capacité, tous comptes confondus : ce n’est pas le plafond de connexions de votre offre, qui ferme en 4003. | Réessayez après le délai porté par Retry-After, ou avec un délai croissant s’il est absent. L’erreur est transitoire par construction. |
500Erreur inattendue | Une panne côté service, jamais causée par la requête. | Réessayez ; si la réponse persiste, signalez-la avec l’en-tête X-Request-Id de la réponse. |
504Requête trop longue | La requête a pris trop de temps pour la fenêtre ou la profondeur demandée (error.code : upstream_timeout). | Réduisez limit ou la plage de temps (since_ms / until_ms), puis réessayez : rejouer la même requête prendrait le même temps. |
Les confusions les plus fréquentes
| Symptôme | Vraie cause |
|---|---|
| 403 alors que la clé est valide | Lisez X-Deny-Reason (l’error.code). plan_family : la famille de cet endpoint n’est pas dans votre offre. plan_websocket : le flux n’y est pas. history_depth : la requête remonte au-delà de la fenêtre de l’offre, et error.details donne la plus grande limit permise. invalid_api_key sans raison apparente : un client HTTP qui perd les en-têtes personnalisés après une redirection ; appelez l’URL finale. |
| 429 alors que le rythme est faible | C’est le quota mensuel du compte qui est épuisé (X-Deny-Reason: quota_exceeded, Retry-After : secondes jusqu’à la remise à zéro), pas la limite de débit. Il repart le premier jour du mois, en UTC, ou dès que l’offre change. |
| 422 sans message clair sur un snapshot | Un champ multi-unités de temps demandé sans son suffixe @tf, ou l’inverse sur un champ à forme unique. |
| 422 sur une profondeur qui paraît raisonnable | Chaque champ du snapshot a son propre plafond, dans sa propre unité. Le CVD s’arrête à 200 fenêtres, les écarts entre places à 50. |
| 422 sur une unité de temps valable ailleurs | Les instruments macro intrajournaliers refusent 30m, et la semaine n’existe que sur les bougies et les indicateurs. |
| Une réponse vide qui n’est pas une erreur | Un stablecoin sur une métrique futures : la clé unavailable dit laquelle des trois raisons s’applique. (Les routes options et ETF ne servent que BTC et ETH : un autre actif est un 422 invalid_asset qui les liste.) |
| 422 sur un paramètre d’apparence anodine | Chaque route refuse un paramètre qu’elle ne déclare pas, et un paramètre passé deux fois ; le snapshot fait de même avec une clé inconnue. La réponse suggère le nom le plus proche : un paramètre mal orthographié n’est jamais ignoré en silence. |
| 422 sur un since_ms qui semble juste | since_ms et until_ms sont en millisecondes. Un horodatage en secondes, en microsecondes ou en nanosecondes est refusé avant toute exécution, comme une borne négative, inversée ou antérieure au 2009-01-03 — sauf since_ms=0, qui veut dire le début des données. |
#Indicateurs
La validation suit un ordre fixe (timeframe, limit, symbole, lecture des indicateurs, règle du plafond, lecture des bougies), et le premier échec l’emporte. Un corps corrigé peut donc révéler une seconde erreur.
| Cas | Code |
|---|---|
| Unité de temps invalide, limit hors de 1 à 1000, symbole inconnu | 422 |
| Liste indicators vide, ou une entrée qui n’est pas un objet | 422 |
| Clé id ou type manquante, id invalide, champ du corps inconnu | 422 |
| Type non pris en charge | 422 |
| Paramètre inconnu pour ce type (obv avec une période, par exemple) | 422 |
| Paramètre du mauvais type, ou inférieur à son minimum | 422 |
| macd ou adosc avec fast supérieur ou égal à slow | 422 |
| sar avec acceleration supérieure à maximum | 422 |
| Identifiant en double | 422 |
| Corps mal formé, plus de 50 instances, champ trop long | 422 |
| limit plus échauffement dépasse 1000 | 422 |
| Données insuffisantes pour cette unité de temps, ou historique de volume trop court pour un indicateur de volume | 422 |
#Flux WebSocket
Toute demande impossible reçoit un message op: error qui dit ce qui était attendu : la liste des symboles valides, celle des canaux, les bornes de depth. Trois situations produisent non pas une erreur mais une fermeture :
| Code | Raison |
|---|---|
| 4003 · plafond | Plafond de connexions simultanées de l’offre atteint. La raison donne le plafond. Une déconnexion libère aussitôt sa place. |
| 4005 · client trop lent | La file d’envoi a débordé dix fois. Consommez plus vite, ou réduisez le nombre de canaux. La raison distingue les deux cas. |
| 4001 | N’est plus envoyé en pratique : la clé est vérifiée à la poignée de main (401), et une trame auth sur une session ouverte répond authenticated. |
| 4002 | Session jamais authentifiée en cinq secondes. Un cas résiduel, puisque la clé est vérifiée à la poignée de main. |
La clé est vérifiée à la poignée de main, avant l’ouverture : une clé absente reçoit un 401 missing_api_key, une clé inconnue ou révoquée un 401 invalid_api_key (avec WWW-Authenticate), une offre sans le flux (Free) un 403 plan_websocket — à chaque fois la valeur de X-Deny-Reason. Un 429 y signale un rythme d’ouverture trop élevé, et un 503 un service de flux qui a atteint sa capacité, tous comptes confondus : réessayez un peu plus tard. Une fois connecté, un canal dont la famille n’est pas dans l’offre reçoit op: error avec error.code: plan_family et tout l’abonnement est refusé. Voir Limites d’usage.
#Serveur MCP
Les erreurs suivent un seul modèle, Error (HTTP NNN): message [code: …], et un appel d’outil en échec est signalé comme une erreur MCP (isError), avec la même erreur dans structuredContent.error : status, code, message, et param, hint, retry_after_s quand ils s’appliquent. Chaque appel d’outil est jugé comme une requête REST avec la clé du client : un 403 pour une famille hors offre ou un 429 pour le quota reviennent tels quels, avec leur message et leur error.code. Deux familles lui sont propres :
- Requête invalide : champ inconnu, cible manquante (
symbol,asset,series) ou qu’aucun champ demandé n’attend, paramètre que la route choisie n’accepte pas,timeframequ’aucun champ n’utilise, fenêtre datée sur un champ qui n’en a pas. Tout est listé dans un seul message, avant tout appel réseau. - Réponse partielle : dans les cinq outils de données MCP (
get_pair_data,get_options_data…), un champ en erreur n’empêche pas les autres. Il est signalé souserrorset la réponse portepartial: true; l’appel n’échoue que si aucun champ n’aboutit.
Une relance n’a lieu que si l’amont n’a pas répondu (502, 503, 504 ou panne réseau) : un 500 n’est pas rejoué, car chaque requête compte dans le quota du client. Un délai dépassé renvoie Error (HTTP 504) après trente secondes.
#Stratégie de nouvelle tentative
Tout ne se rejoue pas. Rejouer une requête fautive produit exactement la même réponse, et consomme du quota pour rien.
import httpx # pip install httpx
def should_retry(response: httpx.Response) -> bool:
"""What is transient, and what never will be (the official SDKs do the same)."""
# A spent monthly quota only resets on the 1st of the month: fail fast.
if response.status_code == 429 and response.headers.get("X-Deny-Reason") == "quota_exceeded":
return False
# 429 (rate) and 503: the server says when, in Retry-After. 502 and 504:
# a gateway hiccup or a slow query. Retry with an increasing delay.
if response.status_code in (429, 502, 503, 504):
return True
# 500: a fault on the service side, retry once.
if response.status_code == 500:
return True
# 401, 403, 404, 405, 422: the request is at fault. Replaying it as is
# produces exactly the same response.
return False- Respectez
Retry-Afterplutôt que d’inventer un délai : le serveur sait quand il sera prêt. - Bornez le nombre de tentatives. Trois ou quatre suffisent : au-delà, le problème n’est pas passager.
- Ne rejouez pas un 4xx, 429 mis à part. Corrigez la requête.
- Sur le flux, reconnectez-vous avec un délai croissant, et lisez le code de fermeture :
4003, le plafond de l’offre est atteint, une autre connexion du compte doit d’abord se fermer ;4005(client too slow), le client est surchargé, et se reconnecter sans changer la consommation reproduira la coupure. Un503à la poignée de main demande seulement d’attendre.