Errores de la API

Cómo manejar errores de la API de FourA.

Formato de respuesta de error

La API devuelve objetos JSON planos para todos los errores. No hay un objeto error anidado. Cuando un fallo tiene un código legible por máquina, se incluye como un campo de nivel superior: reason en un límite de plan, code en una llamada de proxy sin una salida elegible.

{
  "error": "Invalid API key"
}

Algunos errores incluyen campos adicionales como status, service, retryAfter, current o limits en el nivel superior:

{
  "error": "Rate limit exceeded",
  "status": 429,
  "service": "single",
  "retryAfter": 5,
  "current": { "concurrency": 12, "rpm": 3000 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

Rastrear una request

Cada response de la API (éxito o error) incluye un header X-FourA-Request-Id con un UUID para esa llamada, excepto si el body no puede ser leído por FourA en absoluto (JSON mal formado o un body de más de 100 KB): eso se rechaza antes de asignar un ID. Regístralo de tu lado. Si necesitas preguntar a soporte qué ocurrió con una request específica, ese ID nos permite encontrarla.

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
# ...

Tipos de error

400: Bad Request

El cuerpo de la request no contiene los campos obligatorios, incluye valores no válidos o especifica un destino que la API se niega a consultar.

{
  "error": "Invalid request body format"
}

El mismo 400 también cubre la protección contra SSRF. Si tu url se resuelve en un rango de IP privado, de loopback o reservado por otros motivos (RFC 5735, RFC 6598, bloques reservados de IPv6), la request se rechaza antes de salir de la red 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> es la dirección, o el nombre de host y la dirección a la que se resolvió. Una URL que no se puede parsear, o que no es http:// o https://, recibe el mismo 400.

Un nombre de host que no se puede resolver no se rechaza. La llamada devuelve un HTTP 200 con status: 0 y el motivo (could not resolve <host>: <reason>), como cualquier destino al que FourA no puede acceder, y no se factura.

El JSON mal formado en el body se rechaza de la misma manera, antes de que se lea cualquier campo:

{
  "error": "Invalid JSON in request body"
}

Los campos proxy y ignoreProxies tienen sus propios errores 400. Ambos aceptan los proxy IDs opacos devueltos por respuestas anteriores, por lo que cualquier otro valor no se podrá decodificar:

Mensaje Qué ocurrió
Invalid proxy format El valor proxy no es un proxy ID emitido por FourA. Una dirección de proxy sin procesar genera este error.
Invalid ignoreProxies format Una de las entradas en ignoreProxies no es un proxy ID.
Proxy not found El ID se decodificó correctamente pero ya no apunta a una salida activa. Elige una nueva.
Managed exit: this proxy id cannot be pinned to a request La salida existe, pero no es una que FourA mantendrá abierta para un request específico. El ID de una salida premium genera este error cuando a tu plan no le queda tráfico premium disponible. Reutiliza la sesión en la que se devolvió o ejecuta la llamada a través de POST /api/proxy/ y usa la salida que este elija.

Solución: Comprueba que tu request incluya todos los campos obligatorios, que las URLs utilicen http:// o https://, que el host resuelva a una dirección pública y que cualquier valor de proxy sea un ID copiado textualmente de una respuesta anterior.

Estos son resultados client_error: el request nunca salió de FourA, por lo que no se consumió nada de tu cuenta.

401: Unauthorized

Tu API key falta o no es válida.

Falta la key:

{
  "error": "Missing API key. Include X-API-Key header."
}

Clave no válida:

{
  "error": "Invalid API key"
}

Solución: Verifica que tu header X-API-Key contenga una key válida. Genera una nueva key desde el Dashboard si es necesario.

403: Not in Your Plan

La llamada solicitó un endpoint o un parámetro que tu plan no incluye. La response establece X-FourA-Limit e incluye el mismo código en el body bajo 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 es plan_limit_feature para un endpoint que el plan excluye o para exitCountries en un plan sin segmentación geográfica, y plan_limit_premium para exitClass: premium en un plan sin salidas premium. La cadena error nombra el endpoint o el parámetro.

Un 403 de FourA nunca se debe al sitio de destino: el destino nunca fue contactado. Un 403 devuelto por el destino llega como HTTP 200 con status: 403 dentro del body.

Solución: elimina el parámetro, llama a un endpoint que tu plan incluya o actualiza tu plan. No se establece ningún Retry-After, porque esperar no cambia la respuesta. No se gastó nada: el resultado es rate_limit y solo se factura success.

413: Payload Too Large

El body JSON de la request es más grande de lo que FourA acepta (100 KB). La respuesta no es JSON y no incluye X-FourA-Request-Id, ya que el body se rechaza antes de leerse.

Solución: envía un payload data más pequeño. No se gastó nada.

429: Rate Limited

Dos validaciones distintas responden con 429 y no incluyen los mismos campos.

Los límites de tu propio plan. La response incluye un header X-FourA-Limit que indica qué límite rechazó la llamada y coloca el mismo código en el body bajo 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 es uno de plan_limit_concurrency, plan_limit_rate, plan_limit_browser_daily, plan_limit_credits o plan_limit_bandwidth. Cuando esperar sirve de ayuda, el tiempo de espera se encuentra en retry_after_seconds y en el header Retry-After, nunca en retryAfter. plan_limit_browser_daily no incluye ninguno de los dos, ya que el límite permitido se restablece a medianoche UTC y no en segundos. No se ha gastado nada: el resultado es rate_limit, y solo se factura success.

El límite compartido de la plataforma. Sin header X-FourA-Limit, y el tiempo de espera está en retryAfter:

{
  "error": "Rate limit exceeded",
  "status": 429,
  "service": "single",
  "retryAfter": 5,
  "current": { "concurrency": 12, "rpm": 3000 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

current y limits describen el servicio en todo el tráfico, no tu cuenta. Un rechazo aquí significa que FourA está ocupado.

Solución: Espera el tiempo que indique Retry-After, retry_after_seconds o retryAfter en la respuesta. Si se trata de un límite de concurrencia o de rate limit, limita la cantidad de requests abiertas en lugar de reenviar el lote rechazado. Si se trata de un límite diario o del período de facturación, detén la ejecución. Consulta Rate Limits para ver cada campo y Run Requests in Parallel para conocer el patrón.

500: Server Error

Algo salió mal de nuestro lado.

Solución: Reintenta la request tras una breve pausa. Si el error persiste, consulta la página de estado o contacta a soporte con el X-FourA-Request-Id de la respuesta fallida.

502: Upstream Unavailable

FourA se comunicó con su propio motor pero no pudo utilizar la respuesta.

{
  "error": "Upstream unavailable",
  "details": "..."
}

Solución: Reintenta con un backoff corto. Esto ocurre de nuestro lado, por lo que no tiene costo para ti: el resultado es service_error y solo se factura success.

504: Upstream Timeout

El motor no terminó dentro del tiempo límite para este request.

{
  "error": "Upstream timeout",
  "details": "the backend did not finish inside the time budget for this request"
}

Un 504 está relacionado con el tiempo que tomó el trabajo, no con tu key, tus parámetros ni tu proxy. Los destinos lentos, la resolución de challenges en frío y las páginas pesadas suelen ser las causas habituales.

Solución: Aumenta timeout_ms en la request (Single acepta hasta 120000, Browser hasta 120000, Auto hasta 180000) o reintenta. FourA espera el presupuesto que declaraste más un pequeño margen, por lo que solicitar más tiempo realmente te da más tiempo.

503: Servicio deshabilitado o a máxima capacidad

Un 503 significa que el servicio no está disponible temporalmente por mantenimiento o que el límite de concurrencia de la plataforma está lleno. Ambas formas contienen las mismas keys: error, status, service, retryAfter, current y limits. Diferéncialas por el string error, no por los campos que estén presentes.

{
  "error": "Service disabled",
  "status": 503,
  "service": "single",
  "retryAfter": 60,
  "current": { "concurrency": 0, "rpm": 0 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

Service disabled es por mantenimiento y current muestra 0 en ambos contadores, ya que el request se rechazó antes de medir nada. Service at capacity es la variante de concurrencia, y allí current contiene el uso real de la plataforma. Consulta Rate Limits para ver esa estructura.

Solución: espera retryAfter segundos y vuelve a intentarlo. La página de estado detalla las ventanas de mantenimiento activas.

Existe una tercera variante de 503 sin retryAfter. Significa que el motor detrás de tu endpoint se estaba reiniciando cuando llegó tu llamada:

{
  "error": "Backend service unavailable",
  "backend_status": 503
}

Reintenta después de uno o dos segundos.

Leer fallos de /api/auto/

POST /api/auto/ responde con HTTP 200 siempre que la secuencia se ejecutó, incluso si cada paso falló. El resultado real se encuentra en el cuerpo:

{
  "status": 403,
  "error": "exit blocked by the target defense",
  "attempts": 7,
  "meta": { "rung": "fail", "solved": false, "attempts": 7, "credits": 47 }
}

status es el último estado con el que respondió el objetivo, o 502 cuando ningún intento logró alcanzarlo (504 cuando el presupuesto de tiempo se agotó primero). Un campo de la request que Auto no pueda aceptar (por ejemplo, un timeout_ms menor a 5000 o mayor a 180000) se devuelve de la misma forma: HTTP 200 con "status": 400 y el motivo en error, antes de realizar cualquier intento y sin costo alguno.

Por lo tanto, no bifurques según el estado de transporte para Auto. Lee status y error del body en su lugar. Un valor distinto de 200 genuino desde /api/auto/ significa que FourA rechazó la llamada antes de que comenzara el ladder, o no pudo completarla: 400 (JSON mal formado, o un objetivo privado o reservado), 401, 413, 502, 503 o 504. Los límites, ya sean tuyos o de la plataforma, se devuelven dentro del 200 con su estado en el body.

Cuando un sitio ha fallado varias llamadas de Auto seguidas, Auto responde de inmediato durante un tiempo sin intentar: "error": "target temporarily unservable, retry later", "status": 503 y un retryAfter en segundos. No tiene costo; espera retryAfter segundos.

Un límite de plan alcanzado por una de las subllamadas también se devuelve como HTTP 200. El body es el rechazo en sí, con su reason, más status y meta, y la response incluye el mismo header X-FourA-Limit que un rechazo directo:

{
  "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 }
}

Qué límites detienen la escalera y cuáles solo cierran un peldaño se detalla en Smart Fetch (Auto).

Target-Side Failures Inside 200 OK

No todos los fallos aparecen como un estado HTTP que no sea 2xx. Cuando el destino responde HTTP 200 pero la respuesta de FourA incluye un error (por ejemplo, tus reglas validate rechazaron el body) o el body es una página de verificación reconocida por FourA, el resultado es application_error. Cuando el destino devuelve un estado no 2xx que tus reglas validate no aceptan, el resultado es application_fail y el body se entrega sin cambios.

Ninguno de los dos casos se factura: solo se factura success. Browser también puede responder HTTP 200 con "error": "No available browser slot" cuando todos los navegadores de FourA están ocupados. No se factura; reintenta tras unos segundos. La referencia Outcomes cubre la taxonomía completa.

Una llamada Single a través de un proxy fijado también puede responder HTTP 200 con "error": "The exit gave the same answer for <n> different sites" junto al body. FourA detectó que esa salida entregó la misma página a sitios no relacionados, por lo que la página pertenece a la propia salida y no es la que solicitaste. Es application_error y no se factura. Obtén una salida nueva desde POST /api/proxy/, que descarta este tipo de salidas de forma automática.

Response Encoding

FourA decodifica automáticamente los response bodies a UTF-8. Si el destino sirve windows-1251, gbk, shift_jis, iso-8859-* o cualquier otro charset declarado en el header Content-Type o en una etiqueta HTML <meta charset>, recibes una cadena UTF-8 limpia en el campo data (single, proxy) o body (browser).

Para payloads binarios (imágenes, protobuf, audio sin procesar), establece returnBuffer: true en el request. Single y Proxy devuelven entonces data como un objeto que contiene los bytes sin procesar, {"type": "Buffer", "data": [<byte values>]}, sin aplicar ninguna transcodificación de charset.

Retry Strategy

Una política de reintentos práctica:

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

Los fallos de proxy incluyen un reporte

Una llamada de POST /api/proxy/ que agota los intentos se devuelve como HTTP 200 con un contenedor de error, no como un código de error HTTP. La cadena de error es corta y siempre tiene la misma estructura, por lo que un objeto attemptReport la acompaña con los recuentos:

{
  "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
}

Registra attemptReport.summary junto con el error y sabrás si las salidas estaban bloqueadas, caídas o entregando páginas que tus propias reglas de validate rechazaron. Referencia de campos y qué hacer con cada conteo: Por qué una solicitud proxy agotó los intentos.

Relacionado

Actualizado: 30 de septiembre de 2026