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"
}
Fix: Check that your request includes all required fields, that URLs use http:// or https://, and that the host resolves to a public address.
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.
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 responses include a retryAfter field. The concurrency form also includes current and limits.
{
"error": "Service disabled",
"status": 503,
"retryAfter": 60
}
Fix: Wait retryAfter seconds, then retry. The status page lists active maintenance windows.
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:
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