API Errors
How to handle errors from the FourA API.
Error Response Format
The API returns flat JSON objects for all errors. There's no nested error object or error codes.
{
"error": "Invalid API key"
}
Some errors include extra fields like status, service, retryAfter, current, or limits at the top level:
{
"error": "Rate limit exceeded",
"status": 429,
"service": "single",
"retryAfter": 5,
"current": { "concurrency": 12, "rpm": 3000 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Tracking a Request
Every API response (success or error) includes an X-FourA-Request-Id header with a UUID for that call. Log it on your side. If you need to ask support what happened to a specific request, that ID lets us find it.
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
# ...
Error Types
400: Bad Request
The request body is missing required fields, contains invalid values, or names a target the API refuses to fetch.
{
"error": "Invalid request body format"
}
The same 400 also covers SSRF protection. If your url resolves to a private, loopback, or otherwise reserved IP range (RFC 5735, RFC 6598, IPv6 reserved blocks), the request is rejected before it leaves FourA's network:
{
"error": "Target <ip> resolves to a private/reserved IP"
}
Malformed JSON in the body is refused the same way, before any field is read:
{
"error": "Invalid JSON in request body"
}
The proxy and ignoreProxies fields have their own 400s. Both take the opaque proxy IDs earlier responses returned, so anything else fails to decode:
| Message | What happened |
|---|---|
Invalid proxy format |
The proxy value isn't a proxy ID FourA issued. A raw proxy address lands here. |
Invalid ignoreProxies format |
One of the entries in ignoreProxies isn't a proxy ID. |
Proxy not found |
The ID decoded cleanly but no longer resolves to a live exit. Pick a fresh one. |
Fix: Check that your request includes all required fields, that URLs use http:// or https://, that the host resolves to a public address, and that any proxy value is an ID copied verbatim from an earlier response.
401: Unauthorized
Your API key is missing or invalid.
Missing key:
{
"error": "Missing API key. Include X-API-Key header."
}
Invalid key:
{
"error": "Invalid API key"
}
Fix: Verify your X-API-Key header contains a valid key. Generate a new key from the Dashboard if needed.
429: Rate Limited
You've sent too many requests in a short period.
{
"error": "Rate limit exceeded",
"status": 429,
"service": "single",
"retryAfter": 5,
"current": { "concurrency": 12, "rpm": 3000 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Fix: Wait for the number of seconds in retryAfter before sending more requests. See Rate Limits for details.
500: Server Error
Something went wrong on our side.
Fix: Retry the request after a short delay. If the error persists, check the status page or contact support with the X-FourA-Request-Id from the failed response.
502: Upstream Unavailable
FourA reached its own engine but couldn't use the reply.
{
"error": "Upstream unavailable",
"details": "..."
}
Fix: Retry with a short backoff. This is on our side, so it costs you nothing: the outcome is service_error and only success is billed.
504: Upstream Timeout
The engine didn't finish inside the time budget for this request.
{
"error": "Upstream timeout",
"details": "the backend did not finish inside the time budget for this request"
}
A 504 is about how long the work took, not about your key, your parameters, or your proxy. Slow targets, cold challenge solves, and big pages are the usual causes.
Fix: Raise timeout_ms on the request (Single accepts up to 120000, Browser up to 120000, Auto up to 180000), or retry. FourA waits for the budget you declared plus a small margin, so asking for more time genuinely buys more time.
503: Service Disabled or At Capacity
A 503 means either the service is temporarily unavailable for maintenance or you've hit the concurrency limit. Both forms carry the same keys: error, status, service, retryAfter, current, and limits. Tell them apart by the error string, not by which fields are present.
{
"error": "Service disabled",
"status": 503,
"service": "single",
"retryAfter": 60,
"current": { "concurrency": 0, "rpm": 0 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Service disabled is maintenance and current reads 0 for both counters, because the request was turned away before anything was measured. Service at capacity is the concurrency form, and there current holds your real usage. See Rate Limits for that shape.
Fix: Wait retryAfter seconds, then retry. The status page lists active maintenance windows.
A third 503 shape has no retryAfter. It means the engine behind your endpoint was restarting when your call arrived:
{
"error": "Backend service unavailable",
"backend_status": 503
}
Retry after a second or two.
Reading Failures from /api/auto/
POST /api/auto/ answers with HTTP 200 whenever the ladder ran, even when every rung failed. The real outcome lives in the body:
{
"status": 0,
"error": "all attempts failed",
"attempts": 7,
"meta": { "rung": "fail", "solved": false, "attempts": 7, "credits": 47 }
}
So don't branch on the transport status for Auto. Read status and error from the body instead. A genuine non-200 from /api/auto/ means FourA rejected the call before the ladder started: 401, 400, 429, or 503, all documented above.
Target-Side Failures Inside 200 OK
Not every failure shows up as a non-2xx HTTP status. When the target site returns HTTP 200 with an error payload, FourA still hands you the body but classifies the request as application_error. When the target returns a non-2xx your validate rules don't accept, the outcome is application_fail and the body comes through unchanged.
Both cases are billable as if the request worked at the wire level. The Outcomes reference covers the full taxonomy.
Response Encoding
FourA auto-decodes response bodies to UTF-8. If the target serves windows-1251, gbk, shift_jis, iso-8859-*, or any other charset declared in the Content-Type header or an HTML <meta charset> tag, you receive a clean UTF-8 string in the data (single, proxy) or body (browser) field.
For binary payloads (images, protobuf, raw audio), set returnBuffer: true on the request. The body comes back as a base64 buffer with no charset transcoding applied.
Retry Strategy
A practical retry policy:
import time
import requests
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 {}
retry_after = body.get("retryAfter", 2 ** attempt)
request_id = resp.headers.get("X-FourA-Request-Id", "?")
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/404 won't fix themselves
raise RuntimeError(f"{resp.status_code} (request {request_id}): {body.get('error')}")
raise RuntimeError(f"Exhausted {max_retries} retries")
Related
- Rate Limits: Concurrency and RPM details
- Request Outcomes: The seven outcome values explained
- Common Issues: Symptoms, causes, fixes
- Anti-Bot Defenses: When the body is a challenge page rather than an error