Erreurs API
Comment gérer les erreurs de l'API FourA.
Format des réponses d'erreur
L'API renvoie des objets JSON plats pour toutes les erreurs. Il n'y a pas d'objet error imbriqué. Lorsqu'un échec comporte un code lisible par une machine, il s'agit d'un champ de premier niveau : reason en cas de limite de forfait atteinte, code lors d'un appel proxy sans sortie éligible.
{
"error": "Invalid API key"
}
Certaines erreurs incluent des champs supplémentaires comme status, service, retryAfter, current ou limits au niveau supérieur :
{
"error": "Rate limit exceeded",
"status": 429,
"service": "single",
"retryAfter": 5,
"current": { "concurrency": 12, "rpm": 3000 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Suivre une requête
Chaque réponse API (succès ou erreur) inclut un header X-FourA-Request-Id avec un UUID pour cet appel, sauf si le corps ne peut pas du tout être lu par FourA (JSON malformé ou corps supérieur à 100 Ko) : celui-ci est refusé avant qu'un identifiant ne soit attribué. Enregistrez-le de votre côté. Si vous devez contacter le support pour savoir ce qui est arrivé à une requête spécifique, cet identifiant nous permet de la retrouver.
curl -i -X POST https://eu.api.foura.ai/api/single/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"method": "GET", "url": "https://example.com"}'
# HTTP/1.1 200 OK
# X-FourA-Request-Id: 9f1c4e6c-7b2a-4d3e-8a1f-2c9d8e4a3b15
# Content-Type: application/json
# ...
Types d'erreurs
400: Bad Request
Le corps de la requête ne contient pas les champs requis, comporte des valeurs non valides ou indique une cible que l'API refuse de récupérer.
{
"error": "Invalid request body format"
}
Le même code 400 couvre également la protection contre le SSRF. Si votre url résout vers une plage d'adresses IP privée, de bouclage ou réservée (RFC 5735, RFC 6598, blocs réservés IPv6), la requête est rejetée avant de quitter le réseau de FourA :
{
"error": "Refusing to fetch <target>: target resolves to a private or reserved IP range. The FourA scraping API only forwards requests to public internet hosts."
}
<target> est l'adresse, ou le nom d'hôte et l'adresse à laquelle il a été résolu. Une URL qui ne peut pas être analysée, ou qui n'est ni http:// ni https://, renvoie la même erreur 400.
Un nom d'hôte impossible à résoudre n'est pas refusé. L'appel renvoie un code HTTP 200 avec status: 0 et la raison (could not resolve <host>: <reason>), comme pour toute cible que FourA ne peut pas joindre, et il n'est pas facturé.
Un JSON malformé dans le corps de la requête est refusé de la même manière, avant la lecture du moindre champ :
{
"error": "Invalid JSON in request body"
}
Les champs proxy et ignoreProxies ont leurs propres erreurs 400. Tous deux acceptent les identifiants opaques de proxy renvoyés par les réponses précédentes, donc toute autre valeur échoue au décodage :
| Message | Que s'est-il passé |
|---|---|
Invalid proxy format |
La valeur proxy n'est pas un identifiant de proxy émis par FourA. Une adresse proxy brute aboutit ici. |
Invalid ignoreProxies format |
L'une des entrées de ignoreProxies n'est pas un identifiant de proxy. |
Proxy not found |
L'identifiant a été décodé correctement mais ne correspond plus à une sortie active. Choisissez-en une nouvelle. |
Managed exit: this proxy id cannot be pinned to a request |
La sortie existe, mais FourA ne la maintiendra pas ouverte pour une requête nommée. L'identifiant d'une sortie premium aboutit ici lorsque votre forfait n'a plus de trafic premium disponible. Réutilisez la session avec laquelle il a été renvoyé, ou exécutez l'appel via POST /api/proxy/ et acceptez la sortie choisie. |
Solution : Vérifiez que votre requête inclut tous les champs requis, que les URL utilisent http:// ou https://, que l'hôte résout vers une adresse publique, et que toute valeur proxy est un identifiant copié textuellement depuis une réponse précédente.
Il s'agit de résultats client_error : la requête n'a jamais quitté FourA, donc rien n'a été débité de votre compte.
401: Unauthorized
Votre clé API est manquante ou invalide.
Clé manquante :
{
"error": "Missing API key. Include X-API-Key header."
}
Clé non valide :
{
"error": "Invalid API key"
}
Solution : Vérifiez que votre header X-API-Key contient une clé valide. Générez une nouvelle clé depuis le Dashboard si nécessaire.
403 : Non inclus dans votre forfait
L'appel a demandé un endpoint ou un paramètre que votre forfait n'inclut pas. La response définit X-FourA-Limit et place le même code dans le body sous reason :
{
"error": "The browser endpoint is not included in your plan (Free). Upgrade to use it.",
"reason": "plan_limit_feature",
"documentation": "https://foura.ai/prices"
}
reason correspond à plan_limit_feature pour un endpoint exclu par votre forfait ou pour exitCountries sur un forfait sans ciblage géographique, et à plan_limit_premium pour exitClass: premium sur un forfait sans sorties premium. La chaîne error indique le nom du endpoint ou du paramètre.
Une erreur 403 émise par FourA ne concerne jamais le site cible : la cible n'a jamais été contactée. Une erreur 403 renvoyée par la cible arrive sous la forme d'un code HTTP 200 avec status: 403 dans le corps de la réponse.
Solution : Supprimez le paramètre, appelez un endpoint inclus dans votre forfait ou changez d'offre. Aucun en-tête Retry-After n'est défini, car attendre ne changera pas la réponse. Rien n'a été débité : le résultat est rate_limit, et seul success est facturé.
413: Payload Too Large
Le corps de la request JSON dépasse la taille acceptée par FourA (100 KB). La réponse n'est pas au format JSON et ne contient pas de X-FourA-Request-Id, car le corps est rejeté avant d'être lu.
Solution : Envoyez un payload data plus petit. Rien n'a été débité.
429: Rate Limited
Deux vérifications distinctes renvoient une 429, et elles ne contiennent pas les mêmes champs.
Les limites de votre forfait. La response définit un header X-FourA-Limit indiquant quelle limite a rejeté l'appel et inclut le même code dans le corps sous la clé reason :
{
"error": "Concurrency limit reached: your plan allows 50 simultaneous proxy request(s). Retry when an in-flight request finishes.",
"reason": "plan_limit_concurrency",
"documentation": "https://foura.ai/prices",
"limit": 50,
"in_flight": 51,
"retry_after_seconds": 1
}
reason est l'un des suivants : plan_limit_concurrency, plan_limit_rate, plan_limit_browser_daily, plan_limit_credits ou plan_limit_bandwidth. Lorsqu'une attente est utile, la durée d'attente se trouve dans retry_after_seconds et dans le header Retry-After, jamais dans retryAfter. plan_limit_browser_daily ne contient ni l'un ni l'autre, car le quota est réinitialisé à minuit UTC et non en secondes. Rien n'a été dépensé : le résultat est rate_limit, et seul success est facturé.
Le quota partagé de la plateforme. Aucun header X-FourA-Limit, et l'attente se trouve dans retryAfter :
{
"error": "Rate limit exceeded",
"status": 429,
"service": "single",
"retryAfter": 5,
"current": { "concurrency": 12, "rpm": 3000 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
current et limits décrivent le service sur l'ensemble du trafic, pas votre compte. Un refus ici signifie que FourA est occupé.
Solution : Attendez la durée indiquée par Retry-After, retry_after_seconds ou retryAfter présente dans la réponse. En cas de dépassement de concurrence ou de rate limit, limitez le nombre de requêtes simultanées ouvertes plutôt que de renvoyer le lot refusé. En cas de limite quotidienne ou liée à la période de facturation, arrêtez l'exécution. Consultez Rate Limits pour le détail de chaque champ et Exécuter des requêtes en parallèle pour le modèle à suivre.
500: Server Error
Une erreur s'est produite de notre côté.
Solution : Réessayez la requête après un court délai. Si l'erreur persiste, vérifiez la page de statut ou contactez le support en fournissant le X-FourA-Request-Id de la réponse en échec.
502: Upstream Unavailable
FourA a contacté son propre moteur mais n'a pas pu utiliser la réponse.
{
"error": "Upstream unavailable",
"details": "..."
}
Solution : Réessayez avec un court backoff. Cela vient de notre côté, donc cela ne vous coûte rien : le résultat est service_error et seul success est facturé.
504: Upstream Timeout
Le moteur ne s'est pas exécuté dans le temps imparti pour cette request.
{
"error": "Upstream timeout",
"details": "the backend did not finish inside the time budget for this request"
}
Une erreur 504 concerne la durée d'exécution du traitement, et non votre clé, vos paramètres ou votre proxy. Les cibles lentes, la résolution initiale de challenges et les pages volumineuses en sont les causes habituelles.
Solution : Augmentez timeout_ms sur la request (Single accepte jusqu'à 120000, Browser jusqu'à 120000, Auto jusqu'à 180000), ou réessayez. FourA attend le délai que vous avez défini plus une petite marge, donc allouer plus de temps vous permet réellement d'en obtenir davantage.
503 : Service désactivé ou à pleine capacité
Une erreur 503 signifie soit que le service est temporairement indisponible pour maintenance, soit que la limite de requêtes simultanées de la plateforme est atteinte. Les deux cas contiennent les mêmes clés : error, status, service, retryAfter, current et limits. Distinguez-les grâce à la chaîne error, et non selon les champs présents.
{
"error": "Service disabled",
"status": 503,
"service": "single",
"retryAfter": 60,
"current": { "concurrency": 0, "rpm": 0 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Service disabled correspond à une maintenance et current affiche 0 pour les deux compteurs, car la request a été rejetée avant toute mesure. Service at capacity est la variante de concurrence, et dans ce cas current contient l'utilisation réelle de la plateforme. Consultez Rate Limits pour cette structure.
Solution : Attendez retryAfter secondes, puis réessayez. La page d'état répertorie les fenêtres de maintenance actives.
Une troisième structure 503 ne comporte aucun retryAfter. Elle signifie que le moteur derrière votre endpoint redémarrait à l'arrivée de votre appel :
{
"error": "Backend service unavailable",
"backend_status": 503
}
Réessayez après une seconde ou deux.
Lecture des échecs depuis /api/auto/
POST /api/auto/ répond avec un code HTTP 200 chaque fois que l'échelle a été exécutée, même si chaque échelon a échoué. Le résultat réel se trouve dans le corps :
{
"status": 403,
"error": "exit blocked by the target defense",
"attempts": 7,
"meta": { "rung": "fail", "solved": false, "attempts": 7, "credits": 47 }
}
status est le dernier statut renvoyé par la cible, ou 502 si aucune tentative ne l'a atteinte (504 si le budget de temps a expiré avant). Un champ de requête qu'Auto ne peut pas accepter (par exemple un timeout_ms inférieur à 5000 ou supérieur à 180000) revient de la même manière: HTTP 200 avec "status": 400 et la raison dans error, avant toute tentative et sans aucun coût.
Ne basez donc pas vos branchements conditionnels sur le statut de transport pour Auto. Lisez plutôt status et error dans le corps de la réponse. Un véritable statut non-200 renvoyé par /api/auto/ signifie que FourA a rejeté l'appel avant le début de la cascade, ou n'a pas pu le terminer: 400 (JSON malformé, ou cible privée ou réservée), 401, 413, 502, 503 ou 504. Les limites, qu'il s'agisse des vôtres ou de celles de la plateforme, sont renvoyées dans la réponse 200 avec leur statut dans le corps.
Lorsqu'un site fait échouer plusieurs appels Auto consécutifs, Auto répond directement pendant un certain temps sans relancer de tentative: "error": "target temporarily unservable, retry later", "status": 503 et un retryAfter en secondes. Cela ne coûte rien; attendez retryAfter secondes.
Une limite de forfait atteinte par l'un des sous-appels revient également sous la forme d'un HTTP 200. Le corps contient le refus lui-même, avec son reason, ainsi que status et meta, et la réponse porte le même en-tête X-FourA-Limit qu'un refus direct:
{
"status": 429,
"error": "Credit limit reached: 75,000 of 75,000 credits used this billing period. Upgrade your plan or wait for the reset.",
"reason": "plan_limit_credits",
"documentation": "https://foura.ai/prices",
"used": 75000,
"hard_stop": 75000,
"retry_after_seconds": 86400,
"resets_at": "2026-10-01T00:00:00.000Z",
"meta": { "rung": "fail", "solved": false, "attempts": 1, "credits": 0 }
}
Les limites qui interrompent l'escalade et celles qui ferment uniquement un échelon sont détaillées dans Smart Fetch (Auto).
Échecs côté cible au sein d'un statut 200 OK
Tous les échecs ne se traduisent pas par un statut HTTP non-2xx. Lorsque la cible répond HTTP 200 mais que la réponse de FourA contient un error (vos règles validate ont rejeté le corps, par exemple) ou que le corps est une page de vérification reconnue par FourA, le résultat est application_error. Lorsque la cible renvoie un code non-2xx que vos règles validate n'acceptent pas, le résultat est application_fail et le corps est transmis sans modification.
Aucun de ces deux cas n'est facturé: seul success l'est. Browser peut également renvoyer un statut HTTP 200 avec "error": "No available browser slot" lorsque tous les navigateurs de FourA sont occupés. Ce cas n'est pas facturé; réessayez après quelques secondes. La référence des Résultats couvre l'ensemble de la taxonomie.
Un appel Single via un proxy épinglé peut également renvoyer un statut HTTP 200 avec "error": "The exit gave the same answer for <n> different sites" à côté du corps. FourA a détecté que cette sortie renvoyait la même page à des sites non liés; la page provient donc de la sortie elle-même et non de celle demandée. Il s'agit de application_error et ce n'est pas facturé. Obtenez une nouvelle sortie depuis POST /api/proxy/, qui contourne automatiquement ce type de sortie.
Encodage des réponses
FourA décode automatiquement les corps de réponse en UTF-8. Si la cible fournit du windows-1251, gbk, shift_jis, iso-8859-* ou tout autre jeu de caractères déclaré dans l'en-tête Content-Type ou une balise HTML <meta charset>, vous recevez une chaîne UTF-8 propre dans le champ data (single, proxy) ou body (browser).
Pour les charges utiles binaires (images, protobuf, audio brut), définissez returnBuffer: true sur la requête. Single et Proxy renvoient alors data sous la forme d'un objet contenant les octets bruts, {"type": "Buffer", "data": [<byte values>]}, sans aucun transcodage de jeu de caractères.
Stratégie de nouvelle tentative
Une politique de nouvelle tentative pratique:
import time
import requests
# Plan limits that no short wait will clear: stop the run instead of retrying.
STOP_ON = {
"plan_limit_feature",
"plan_limit_premium",
"plan_limit_browser_daily",
"plan_limit_credits",
"plan_limit_bandwidth",
}
def make_request(url, payload, api_key, max_retries=3):
for attempt in range(max_retries):
resp = requests.post(
url,
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json=payload,
)
if resp.status_code == 200:
return resp.json()
body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
request_id = resp.headers.get("X-FourA-Request-Id", "?")
limit = resp.headers.get("X-FourA-Limit")
if limit in STOP_ON:
raise RuntimeError(f"{limit} (request {request_id}): {body.get('error')}")
# Plan limits send Retry-After and retry_after_seconds; platform limits send retryAfter.
header = resp.headers.get("Retry-After")
retry_after = (
int(header) if header and header.isdigit()
else body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
)
if resp.status_code in (429, 503):
time.sleep(retry_after)
continue
if resp.status_code >= 500: # 500, 502, 503, 504 are all ours to fix
time.sleep(2 ** attempt)
continue
# 400/401/403/404 won't fix themselves
raise RuntimeError(f"{resp.status_code} (request {request_id}): {body.get('error')}")
raise RuntimeError(f"Exhausted {max_retries} retries")
Les échecs de proxy contiennent un rapport
Un appel POST /api/proxy/ qui épuise ses tentatives renvoie un code HTTP 200 avec une enveloppe d'erreur, et non un code d'erreur HTTP. La chaîne d'erreur est courte et conserve toujours la même structure, un objet attemptReport est donc joint pour fournir le détail des compteurs :
{
"error": "Download maxTry limit reached",
"attemptReport": {
"total": 25,
"noResponse": 0,
"defense": 0,
"contentRejected": 25,
"statusRejected": 0,
"other": 0,
"vendors": [],
"profilesTried": ["default"],
"summary": "25 attempt(s): 25 returned HTTP 200 with no defense present and were rejected only by your validate.data - the page was fetched, your content rule did not match it"
},
"total": 34.812
}
Journalisez attemptReport.summary à côté de l'erreur et vous saurez si les sorties étaient bloquées, indisponibles, ou si elles renvoyaient des pages rejetées par vos propres règles validate. Référence des champs et mesures à prendre pour chaque compteur : Pourquoi une requête proxy a épuisé ses tentatives.
Liens associés
- Rate limits : Limites des plans, concurrence et détails RPM
- Exécuter des requêtes en parallèle : Rester sous la limite de concurrence de votre plan
- Résultats de requête : Explication des sept valeurs de résultat
- Problèmes fréquents : Symptômes, causes et solutions
- Vérifications de site : Quand le corps est une page de challenge plutôt qu'une erreur
- Pourquoi une requête proxy a épuisé ses tentatives : Comprendre
attemptReport