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

  1. Always check the status code before parsing the response body.
  2. Implement retries with backoff for 429, 500, and 503 errors.
  3. Log error responses for debugging: the error field contains the message. From the MCP server, log code first since it's the stable retry signal.
  4. Set reasonable timeouts: 30 seconds for browser requests, 15 seconds for single requests.
  5. Respect retryAfter when 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

Last updated: May 20, 2026