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 }
}
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.
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
}
{
"error": "Service at capacity",
"status": 503,
"service": "proxy",
"retryAfter": 2,
"current": { "concurrency": 500, "rpm": 1200 },
"limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}
Fix: Don't retry immediately. Wait for the number of seconds in retryAfter, then try again. Maintenance windows typically resolve within minutes. For concurrency limits, reducing parallel requests helps long-term.
Response Fields
Not all fields appear in every error. Only error is guaranteed.
| Field | Type | Description |
|---|---|---|
error |
string | Plain English error message |
status |
number | HTTP status code |
service |
string | Which service returned the error: single, proxy, browser, or api |
retryAfter |
number | Seconds to wait before retrying |
current.concurrency |
number | Your active requests right now (rate limit responses) |
current.rpm |
number | Your requests in the last 60 seconds (rate limit responses) |
limits.maxConcurrency |
number | Maximum simultaneous requests allowed (rate limit responses) |
limits.maxRpm |
number | Maximum requests per minute allowed (rate limit responses) |
MCP Server Error Codes
When you call FourA through the MCP server, every isError: true response carries a structuredContent envelope with a stable code field. Use it for retry logic without parsing prose.
{
"service": "single",
"code": "rate_limited",
"error": "Rate limit exceeded",
"status": 429,
"retryAfter": 5
}
| Code | HTTP | Meaning | Retry safe? |
|---|---|---|---|
ssrf_blocked |
n/a | Target IP in a private or reserved range | No, change the URL |
upstream_non_json |
varies | Upstream returned malformed body | Maybe, investigate |
output_validation_failed |
n/a | The MCP server's outputSchema rejected the upstream response |
Maybe, report as a bug |
bad_request |
400 | Input shape rejected | No, fix arguments |
auth_failed |
401 | Key missing, invalid, or deactivated | No, fix the key |
forbidden |
403 | Authenticated but target blocked | No, or switch to foura_proxy |
not_found |
404 | Target or endpoint missing | No |
rate_limited |
429 | RPM cap hit | Yes, wait retryAfter |
at_capacity |
503 | Concurrency cap hit | Yes, wait retryAfter |
service_disabled |
503 | Plan or account disabled for this tool | Contact support |
service_unavailable |
503 | Generic 503 | Yes, short backoff |
upstream_error |
500+ | Upstream 5xx | Yes, exponential backoff |
upstream_client_error |
4xx | Other 4xx | Usually no |
upstream_unknown |
other | Defensive, should not occur in practice | Investigate |
The MCP server also returns top-level HTTP errors for its own transport layer:
| HTTP | Header / body | Meaning |
|---|---|---|
| 400 | Unsupported MCP-Protocol-Version: ... |
Header version not in the bundled SDK |
| 401 | WWW-Authenticate: Bearer realm="foura-mcp", resource_metadata="..." |
Missing or malformed Authorization header |
| 403 | Origin ... is not in the allowlist / Host ... is not in the allowlist |
DNS-rebinding defense (CVE-2025-66414) |
| 405 | Method not allowed in stateless mode. Use POST /mcp. |
GET/DELETE on /mcp |
| 413 | (Express default) | Request body exceeded 256 KB cap |
Best Practices
- Always check the status code before parsing the response body.
- Implement retries with backoff for 429, 500, and 503 errors.
- Log error responses for debugging: the
errorfield contains the message. From the MCP server, logcodefirst since it's the stable retry signal. - Set reasonable timeouts: 30 seconds for browser requests, 15 seconds for single requests.
- Respect
retryAfterwhen present. It's a top-level field, not nested.
Retry Example (Python)
import requests
import time
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
resp = requests.post(
"https://eu.api.foura.ai/api/single/",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
json={"method": "GET", "url": url}
)
if resp.status_code in (429, 503):
wait = resp.json().get("retryAfter", 30)
time.sleep(wait)
continue
return resp
raise Exception("Max retries exceeded")
Next Steps
- API Endpoints: Full endpoint reference
- Rate Limits: Understand request limits
- Troubleshooting: Solutions to frequent problems
- MCP Server: Using FourA from Claude Desktop / Code / Cursor