API Endpoints Reference
A reference for all FourA API endpoints with request parameters and response formats.
Base URL
https://eu.api.foura.ai/api
Authentication
Every request requires your API key in the X-API-Key header:
curl -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"}'
Create and manage API keys in the Dashboard. Keys use the pk_live_ prefix.
Response Headers
Responses from /api/* carry two correlation headers:
| Header | Value | Description |
|---|---|---|
X-FourA-Request-Id |
UUID | Unique ID assigned to the request. Returned on every response, including 4xx and 5xx, except a body FourA can't read at all: 400 Invalid JSON in request body and 413 are refused before an ID is assigned. Log it on your side. |
X-FourA-Credits |
integer | Credits spent on this request. Returned on every response that reached an engine, success or failure (the work was done either way). A call FourA refused before any engine ran it (a missing or invalid key, a plan or platform limit, a refused target or proxy ID) carries none. See Request Outcomes for which outcomes are billable. |
The same request ID keys the request and response payload preview in the Dashboard's Activity Log (kept 24 hours, last 200 per key), so you can look up the exact request later and replay it from Activity straight into the Playground. Include it when you contact support, and it pinpoints the request in seconds.
$ 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/2 200
content-type: application/json
x-foura-request-id: 8f3e2a14-7b6c-4d1a-9e2f-5a3b8c1d4e7f
x-foura-credits: 2
...
See Response Headers for the full list and usage tips.
Endpoints
Using these endpoints via MCP? The
@fouradata/mcpserver wraps all four endpoints as native MCP tools (foura_auto,foura_single,foura_proxy,foura_browser) with the same input shapes plus anoffload_largeopt-in for token-friendly large-response handling.
FourA provides four request endpoints, each optimized for a different scenario:
| Endpoint | Best for |
|---|---|
POST /auto/ |
Smart fetch. You pass a URL, FourA picks the cheapest path that works (direct, rotated proxy, or browser) and remembers what works per host. |
POST /single/ |
Fast HTTP requests, static pages, APIs |
POST /proxy/ |
Protected sites with automatic proxy rotation, optional target-visible country scoping |
POST /browser/ |
JavaScript-rendered pages, SPAs |
GET /profiles |
The browser-profile catalogue for single and proxy. Public, no API key. |
For a deeper walkthrough of when to pick each, see Choosing the Right Endpoint and the Smart Fetch guide.
Target URL Restrictions
Targets that resolve to private, loopback, or reserved IP ranges (RFC 5735, RFC 6598, IPv6 reserved blocks) are refused with a 400 before the request leaves FourA. Only public hostnames and IPs are forwarded.
{ "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." }
A target on FourA's own API host (eu.api.foura.ai or api.foura.ai) is refused with a 400 too, on all four endpoints, before anything is fetched or charged. The one page there FourA fetches is /inspect.
{ "error": "Refusing to fetch <url>: that is the FourA API itself, not a page. A link taken from a page was probably resolved against the API URL; resolve it against the page's own URL." }
Smart Fetch (Auto)
POST /api/auto/
You pass a URL plus optional validate rules. FourA walks a cost-aware ladder (cheap direct probe, rotated proxy, full browser) and stops at the first rung that returns a response your rules accept. On repeat calls to the same host, a warm session is replayed instead, so the second hit is cheap.
You don't tune retries, pool sizes, or proxy counts. FourA learns them per host.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url |
string | Yes | - | Target URL |
method |
string | No | "GET" |
HTTP method |
headers |
[string, string][] | No | - | Custom headers as [name, value] pairs |
data |
any | No | - | Request body for non-GET requests |
validate |
object | No | - | Success criteria, same shape as Single Request's validate (see below). Tell auto what a real page looks like so it can tell content from a challenge page. |
returnSession |
boolean | No | true |
Include the winning session (proxy, cookies, userAgent) in the response so you can replay it through /api/single/ or /api/browser/. |
forceProxy |
boolean | No | true |
Always route through a rotating proxy. Set false to allow the cheaper direct path when the target permits it (some defenses are stricter on proxy traffic). |
timeout_ms |
integer | No | 120000 |
Total time budget for the whole call, in milliseconds. All sub-attempts run inside this budget. Min 5000, max 180000. |
ignoreProxies |
string[] | No | - | Proxy IDs to avoid on every sub-attempt. Use IDs returned by previous /api/auto/ or /api/proxy/ responses. |
followRedirects |
integer | No | 5 |
Max redirects to follow on the cheap ladder rungs. 0 to disable. Max 20. |
exitClass |
string | No | - | standard or premium, as on Proxy Request. premium lets the call escalate to a premium exit once the standard pool can't reach the page. Requires a plan that includes premium exits. See exitClass on Smart Fetch. |
Response
{
"status": 200,
"data": "<!doctype html>...",
"headers": [{"content-type": "text/html"}],
"meta": {
"rung": "cache",
"solved": false,
"attempts": 1,
"credits": 2
},
"session": {
"proxy": "A1B2C3",
"cookies": [{"name": "session", "value": "abc", "domain": "example.com"}],
"userAgent": "Mozilla/5.0..."
}
}
| Field | Type | Description |
|---|---|---|
status |
number | HTTP status from the target. |
data |
string | Response body as text, whichever rung served it. A JSON page comes back as JSON text, so parse it yourself. |
headers |
array or object | Target response headers. Single and proxy rungs return an array of per-hop header objects; browser rungs return a flat object. |
meta.rung |
string | Which ladder rung delivered the response. One of: probe (cheap direct request), proxy (rotating proxy), browser (full browser render), cache (warm session replayed), warmup (the site's entry page was fetched first and its cookies opened the deep URL), or fail (no rung produced an accepted response). |
meta.solved |
boolean | Whether the page needed an extra step (a challenge page) and it was completed during this call. |
meta.attempts |
number | Sub-attempts made before success. |
meta.credits |
number | Total credits spent on this call. Matches X-FourA-Credits. |
meta.exitClass |
string | Which class of exit delivered, present when the request named an exitClass: premium or standard. The same value travels in the X-FourA-Exit-Class response header. |
session.proxy |
string | Encoded ID of the proxy that delivered the response. Reuse it on a Single or Browser request. Present when returnSession is true. |
session.cookies |
array | Cookies from the winning attempt. Present when returnSession is true. |
session.userAgent |
string | User-Agent used on the winning attempt. Present when returnSession is true. |
error |
string | Error message if the call failed. |
Example
curl -X POST https://eu.api.foura.ai/api/auto/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/product/42",
"validate": {"data": {"accept": ["Add to cart"]}}
}'
Notes
- Auto is a coordinator. It calls Single, Proxy, or Browser internally and forwards your API key to each sub-call. The Auto call is one request in your Activity Log and on your Overview, with the sum of its sub-calls' credits; the sub-calls are listed under it as its attempts, and they never count as requests of their own.
- Pass
validate.data.acceptwith a substring that only the real page contains. Without it, auto can't tell a real 200 from a challenge interstitial returned with status 200. timeout_mscaps the whole call. A cold first hit to a protected site can take tens of seconds; reused warm sessions usually finish in under a second.
exitClass on Smart Fetch
exitClass works on Smart Fetch as it does on Proxy Request: an allowance, not an instruction. Auto tries the standard pool first. When the standard pool can't reach the page and the request names premium, Auto opens the page in a browser through a premium exit. The session it builds there serves your later calls to the same site that also name premium; calls that don't name it never use it.
Running out is not an error. Once the premium traffic in your plan is spent for the billing period, the call goes on in the standard pool and meta.exitClass says standard. On a plan without premium exits, a call that sends exitClass: premium is refused with a 403 carrying X-FourA-Limit: plan_limit_premium, before any work is done.
Premium traffic is measured as on Proxy Request. A page opened in a browser loads more than the page itself, so a premium Smart Fetch page uses more premium traffic than the same page fetched by Proxy Request. To keep that down, a premium Smart Fetch page loads without images, fonts and media, and without scripts from known advertising, analytics and consent providers.
Single Request
POST /api/single/
Sends an HTTP request with realistic browser-like wire characteristics, without spinning up a real browser. This is the fastest endpoint.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
method |
string | Yes | - | HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
url |
string | Yes | - | Target URL. Use {ts} anywhere in the URL to insert the current timestamp for cache-busting. |
headers |
[string, string][] | No | - | Custom headers as [name, value] pairs |
unblocker |
boolean | No | true |
Send realistic browser headers (User-Agent, Sec-Ch-Ua, Sec-Fetch-*, Accept-Encoding). On by default. Set false to send a plain client signature. |
timeout_ms |
number | No | 15000 | Overall timeout in ms (max: 120000) |
connect_timeout_ms |
number | No | 5000 | Connection timeout in ms |
accept_timeout_ms |
number | No | 5000 | Accept timeout in ms (time to wait for connection acceptance) |
server_response_timeout_ms |
number | No | 15000 | Server response timeout in ms (time to wait for first byte) |
dns_cache_timeout_sec |
number | No | 120 | DNS cache TTL in seconds (max: 240) |
followRedirects |
number | No | disabled | Max redirects to follow (0-20). Omit to disable. |
tryJsonData |
boolean | No | false | Parse response body as JSON if possible |
returnBuffer |
boolean | No | false | Return raw buffer instead of decoded string |
data |
any | No | - | Request body (string or object, auto-serialized to JSON) |
proxy |
string | No | - | Proxy ID from an earlier response, to pin the same exit. Pass the opaque string back verbatim. A raw proxy address is rejected with 400 Invalid proxy format. Some IDs can't be pinned: see Pinning an exit. |
browser |
string | No | Chrome | Browser to present: Chrome, Edge, Safari, Firefox, or Tor. See Browser profiles. |
os |
string | No | - | Operating system to present: Windows, macOS, Android, or iOS. A family name accepts any of its versions. |
version |
string | No | newest | Browser version to present, as listed in the catalogue. The newest match wins when several fit. |
profile |
string | No | - | Exact profile id from GET /api/profiles, instead of the three fields above. |
validate |
object | No | - | Response validation rules (see below) |
Browser profiles
By default a request presents the latest Google Chrome. Some targets accept one browser and refuse another, so browser, os, and version narrow a catalogue of measured profiles, and profile selects one by id.
{
"method": "GET",
"url": "https://example.com",
"browser": "Firefox",
"os": "Windows"
}
Rules:
- Selection requires
unblocker(on by default). Withunblockeroff no browser headers are sent, so the request is refused rather than half-applied. - When several profiles match, the newest version wins.
- A combination the catalogue can't present returns an error naming what is available. The request is never sent as a different browser.
- The same four fields are available inside the
requestobject ofPOST /proxy/.
GET /api/profiles returns the full catalogue and needs no API key:
{
"profiles": [
{ "id": "...", "browser": "Chrome", "version": "...", "os": "...", "osFamily": "macOS" }
],
"default": "..."
}
osFamily is the value to filter on when building a picker; os keeps the release name for display.
Validation Rules
The validate object lets you define conditions for success and failure. If a fail condition matches, the request is treated as failed. If accept conditions are set, only matching responses are treated as successful.
{
"validate": {
"status": { "accept": [200, 201], "fail": [403, 503] },
"headers": { "accept": {"content-type": "application/json"} },
"data": { "accept": ["product"], "fail": ["captcha", "blocked"] }
}
}
| Field | Type | Description |
|---|---|---|
validate.status.accept |
number[] | HTTP status codes to accept |
validate.status.fail |
number[] | HTTP status codes to reject |
validate.headers.accept |
object | Header key-value pairs that must be present |
validate.headers.fail |
object | Header key-value pairs that trigger failure |
validate.data.accept |
string[] | Strings that must appear in the response body |
validate.data.fail |
string[] | Strings in the response body that trigger failure |
Example
curl -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/products",
"timeout_ms": 10000
}'
Response:
{
"status": 200,
"headers": [{"result": {"version": "HTTP/2", "code": 200, "reason": ""}, "content-type": "text/html", "server": "...", "set-cookie": ["session=abc", "tracker=xyz"]}],
"data": "<!doctype html>...",
"total_time": 0.342,
"proxy": "A1B2C3"
}
When the target runs a bot check on the way to the body, the response also carries a defense object naming the vendor and whether the check was cleared:
{
"status": 200,
"data": "<!doctype html>...",
"total_time": 3.61,
"defense": {
"vendor": "sgcaptcha",
"solved": true,
"present": ["sgcaptcha"],
"ms": 3412,
"cookie": "_I_=<clearance>"
}
}
| Field | Type | Description |
|---|---|---|
status |
number | HTTP status code from the target |
headers |
array | One object per redirect hop. Each has a result field with the status line plus every response header. Multi-value headers (Set-Cookie, Link, WWW-Authenticate) come back as arrays of strings. |
data |
string/object | Response body (JSON if tryJsonData is true) |
total_time |
number | Total request time in seconds |
proxy |
string | Encoded ID of the proxy the request went through (only when a proxy was supplied on the request). Reuse it on a follow-up call to pin the same exit. |
defense |
object | Present when the target ran a bot check on this request, or when a retry with the site's own cookies produced the body. defense.solved says whether a check was cleared, defense.retry says whether a retry got you the content. See Site checks for every field and the full list of systems. |
error |
string | Error message if the request failed |
Proxy Request
POST /api/proxy/
Routes your request through rotating proxies with automatic retry on failure. Optionally scope selection to a set of target-visible exit countries.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
request |
object | Yes | - | A single request body (same fields as Single Request above) |
timeout_ms |
number | No | 45000 | Overall timeout for all attempts in ms (max: 120000) |
maxTries |
number | No | 5 | Maximum proxy rotation attempts (max: 90) |
ignoreProxies |
string[] | No | - | Proxy IDs to exclude from rotation (use IDs returned by previous responses) |
exitCountries |
string[] | No | - | Strict allowlist of two-letter target-visible country codes (e.g. ["CZ", "GB"]). Values are trimmed, uppercased, and deduplicated. Proxies with unknown exits are excluded and the request never falls back to an unrequested country. |
exitClass |
string | No | - | standard or premium. premium lets the request escalate to a premium exit when the standard pool is struggling on a protected target. Requires a plan that includes premium exits. |
exitCountries scoping
Selection uses the latest available target-visible country metadata, normally refreshed within about ten minutes. It's not a live geolocation lookup during the request. Don't infer the serving country from the proxy host address.
If the current pool has no match for the requested countries, the response returns HTTP 200 with an error envelope:
{
"error": "No eligible proxy found for exit countries: CZ, GB",
"code": "no_eligible_proxy",
"details": { "exitCountries": ["CZ", "GB"] },
"total": 0.084
}
Preserve the requested scope and retry later. Change or widen it only when your workflow's country requirement explicitly changes.
Example
curl -X POST https://eu.api.foura.ai/api/proxy/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"maxTries": 3,
"exitCountries": ["CZ", "GB"],
"request": {
"method": "GET",
"url": "https://example.com/prices"
}
}'
Response:
{
"status": 200,
"headers": [{"result": {"code": 200}, "content-type": "text/html", "set-cookie": ["a=1", "b=2"]}],
"data": "<!doctype html>...",
"total_time": 1.204,
"proxy": "A1B2C3",
"exitCountry": "CZ",
"total": 2.341
}
| Field | Type | Description |
|---|---|---|
proxy |
string | Encoded identifier of the proxy used. Reuse it on a Single or Browser request by passing it as the proxy field, or skip it on the next Proxy request via ignoreProxies. |
exitCountry |
string | Two-letter target-visible country code of the proxy that served the request. Present only when the request set exitCountries. Always verify it's one of the codes you requested before trusting the response. |
exitClass |
string | Which class of exit served this request, present on a successful response when the request named one. premium means a premium exit returned the body; standard means the standard pool did. A failed call served nothing, so it carries no exitClass; read its attemptReport for what the attempts met. |
total |
number | Outer wall-clock duration in seconds (float). Includes proxy selection, retries, and the successful attempt. total_time is the inner request only; total is always >= total_time. |
profile |
string | The browser profile the rotation chose, present only when it wasn't the one you asked for. Absent means the request went out exactly as written. Pass the id back as profile on follow-up calls to keep the browser that worked. |
error |
string | Error message if the request failed. On a scope miss, code is no_eligible_proxy and details.exitCountries echoes the normalized scope. |
attemptReport |
object | Present on every failed Proxy call. Counts what the attempts ran into, so a blocked pool, a dead pool, and a validate rule that never matched don't all read as the same error. See below. |
All Single Request response fields are also included, defense among them: a proxy attempt that met a bot check reports it the same way Single does.
Why a Proxy Call Failed
Download maxTry limit reached reads the same whichever way the attempts went, so every failed Proxy response carries an attemptReport beside the error:
{
"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
}
| Field | Type | Description |
|---|---|---|
total |
integer | Attempts made |
noResponse |
integer | The exit never answered, so the site was never reached |
defense |
integer | The site answered and a bot check was recognised on that answer |
contentRejected |
integer | HTTP 200, no bot check, rejected only by your validate.data |
statusRejected |
integer | The site answered, no bot check, rejected by your validate.status |
other |
integer | Answered, and none of the above |
vendors |
string[] | Bot-check vendors recognised anywhere in the task |
profilesTried |
string[] | Browser profiles the task sent, in first-use order. default means your request went out untouched. |
summary |
string | One sentence built from the counts, safe to log |
The error string is unchanged, so a client matching on it keeps working. What to do about each count: Why a Proxy Request Ran Out of Tries.
exitClass
Some targets refuse the exits in the standard pool no matter how many are tried. exitClass: premium tells Proxy it may escalate such a request to a premium exit in addition to the standard pool, instead of only rotating within it.
{
"exitClass": "premium",
"request": { "method": "GET", "url": "https://example.com/report" }
}
Three things are worth knowing before you send it.
It is an allowance, not an instruction. The standard pool still races for the answer, and it usually wins. A premium exit joins only when the pool has spent a short budget on the request or the target has visibly refused it. A request that the standard pool answers before any premium exit has been tried is a normal success and costs you no premium traffic. Once a premium exit has been tried, its traffic counts, as described below.
The response tells you what actually served you. When you name a class, the response carries exitClass back:
{
"status": 200,
"exitClass": "premium",
"proxy": "Y2QXVK",
"data": "..."
}
premium means a premium exit returned the body. standard means the standard pool did, which is the answer you also get when a premium exit could not be obtained, and when the premium traffic included in your plan (plus anything you bought on top) is spent for the billing period. Neither is an error, and you can reconcile your premium traffic against these per request rather than against a monthly figure. The same value travels in the X-FourA-Exit-Class response header (see Response Headers).
Premium traffic is measured on the network. A premium attempt counts what it sent and received as it crossed the network, compressed and encrypted as it went, whether or not it returned your page. An attempt that was still running when another exit answered is stopped at once and isn't counted. Premium traffic counts toward your premium allowance and also inside your total bandwidth: the same bytes, reported twice, never added together. When a premium exit delivered the page, its traffic is the request's whole traffic, so the page isn't counted again as standard traffic. Your Usage & Limits page shows total traffic, the premium share of it, and the premium allowance you're measured against.
Omitting the field is not the same as sending standard. Omitting it leaves the decision unstated; sending standard says explicitly that this request must never escalate, which is the way to keep a particular job off premium traffic entirely.
Running out is not an error. A request naming premium after the allowance is spent keeps working: the standard pool serves it and the response says standard. No job stops over a spent allowance.
exitClass: premium needs a plan that includes premium exits. On a plan without them, the request never spends a premium exit: it's either refused with a 403 carrying X-FourA-Limit: plan_limit_premium (see Rate Limits), or served from the standard pool with exitClass: standard in the response. Handle both.
Browser Profile Rotation
Proxy rotates exits. When a site refuses the browser FourA presented rather than the exit it came from, Proxy also moves to another browser family from the catalogue. It adds no attempt: the rotation changes what a retry sends, never whether one happens.
Proxy also remembers, for a while, the family a site last accepted, so a later call to the same site can start on that family instead of the default. The response names it in profile, as it does for any family the rotation chose.
An explicit profile, browser, os, or version on your inner request is never overridden, and neither is a request that carries its own User-Agent or Cookie header, because a clearance is bound to the signature that earned it.
Browser Request
POST /api/browser/
Opens your URL in a Chrome browser instance. The page loads, JavaScript executes, and you get the fully rendered HTML plus the cookie jar.
Request Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url |
string | Yes | - | Target URL |
headers |
object | No | - | Custom headers as key-value pairs |
cookies |
array | No | - | Cookies to set: [{name, value, domain?}] |
userAgent |
string | No | - | Custom User-Agent string |
unblocker |
boolean | No | true |
Completes the check a page asks for before it loads (a challenge page or a similar gate). On by default. Set false to render whatever the page returns, including a challenge page, as it came. |
proxy |
string | No | - | Proxy ID from an earlier response, to pin the same exit. Pass the opaque string back verbatim. A raw proxy address is rejected with 400 Invalid proxy format. |
exitCountry |
string | No | - | Two-letter country code (ISO 3166-1 alpha-2) of the country the request leaves by. Sets the browser's clock to a timezone that matches. See Matching the browser clock to the exit. |
timeout_ms |
number | No | 30000 | Page load timeout in ms (max: 120000) |
checkStatus |
number | No | - | Expected HTTP status (request fails if different) |
checkText |
string | No | - | Text that must appear in the rendered page |
Matching the browser clock to the exit
A page can read the browser's timezone and compare it against the country of the IP it sees. A mismatch is one of the cheapest signals a bot detector has, and it costs you nothing to remove.
Set exitCountry to the country your traffic leaves by and the browser reports a timezone that belongs to it:
{
"url": "https://example.com",
"proxy": "A1B2C3",
"exitCountry": "BR"
}
Rules:
- The value is the exit country, meaning the country the target sees, not where the proxy is hosted. The two disagree often enough to matter.
- Omit it and FourA uses the exit country when it knows one, and otherwise leaves the browser clock alone rather than guessing.
- A country code FourA doesn't recognise is treated the same as leaving the field out. It isn't an error.
- Only the clock follows the country.
Accept-Languageand the content the site serves are untouched, so a page won't switch languages on you.
The userAgent parameter
Send userAgent and that exact string is what the page, its workers, and the target all see. FourA also derives the matching client hints from it (sec-ch-ua, sec-ch-ua-platform, navigator.platform, and the high-entropy values a detector asks for by name), so the request doesn't claim one browser in the header and another in JavaScript.
The userAgent in the response is the one that was presented. That matters when you replay a clearance: a cf_clearance cookie is bound to the exit and the User-Agent that earned it, so send back the string the response reported, not the one you think was used. See Site checks.
Send a non-Chromium string (a Firefox User-Agent, say) and it's presented as-is, with no Chromium brand list attached.
Example
curl -X POST https://eu.api.foura.ai/api/browser/ \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/spa-app",
"timeout_ms": 15000,
"checkText": "product-list"
}'
Response:
{
"status": 200,
"headers": {"content-type": "text/html"},
"body": "<!doctype html>...",
"cookies": [{"name": "session", "value": "abc123", "domain": "example.com", "path": "/", "expires": 1735689600, "httpOnly": true, "secure": true, "sameSite": "Lax"}],
"userAgent": "Mozilla/5.0...",
"defenseSolved": true,
"defenses": {"present": ["cloudflare"], "cleared": ["cloudflare"]},
"proxy": "A1B2C3"
}
| Field | Type | Description |
|---|---|---|
status |
number | HTTP status code from the target |
headers |
object | Response headers |
body |
string or object | Fully rendered page content. String HTML when content-type is HTML; object when the page returned JSON and was auto-parsed. |
cookies |
array | Full cookie objects from the page. Each cookie includes name, value, domain, path, expires, httpOnly, secure, sameSite, and other cookie properties. |
userAgent |
string | Browser User-Agent used |
defenseSolved |
boolean | true if a bot defense was met and genuinely cleared on this call, false otherwise. Decides whether the call costs 5 or 10 credits. |
defenses |
object | present lists every vendor recognised during the page load, cleared lists the ones whose clearance the final page holds. A vendor can appear in present and never in cleared. See Site checks. |
proxy |
string | Encoded ID of the proxy the request went through (only when a proxy was supplied on the request). Reuse it on follow-up calls to keep the same exit. |
error |
string | Error message if the request failed |
Pinning an Exit
A proxy value on a Single or Browser request pins the exit a previous call used. Pass back the opaque ID exactly as it came, never a proxy address.
Three values are refused, all with a 400:
| Error | Meaning |
|---|---|
Invalid proxy format |
The value isn't an ID FourA issued. A raw proxy address lands here. |
Proxy not found |
The ID decoded, but it no longer resolves to a live exit. Take a fresh one from a new call. |
Managed exit: this proxy id cannot be pinned to a request |
The exit exists, but it isn't one FourA will hold open for a named request. A premium exit's ID lands here when your plan has no premium traffic left to spend. Reuse the session it came back on, or run the call through POST /api/proxy/ and take whichever exit it picks. |
A pinned premium exit is metered as premium traffic. The response carries X-FourA-Exit-Class: premium so you can see it per request, and the traffic the exit carried counts toward the premium traffic on your Usage & Limits page as well as toward your total bandwidth, whether or not the site returned the page you wanted. Pinning needs premium exits in your plan and allowance left; otherwise the ID is refused with the managed-exit 400 above.
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Request completed (check inner status for target response) |
| 400 | Invalid request body, parameters, target IP in a private/reserved range, a target on FourA's own API host, or a proxy ID that can't be pinned |
| 401 | Missing or invalid API key |
| 403 | The endpoint or a parameter isn't in your plan. X-FourA-Limit names it: plan_limit_feature or plan_limit_premium. |
| 404 | Not Found: there's no endpoint at that path. |
| 413 | The JSON request body is over 100 KB. The answer isn't JSON and carries no X-FourA-Request-Id. |
| 429 | A plan limit (X-FourA-Limit set) or the platform's shared per-minute allowance (no header) |
| 500 | Internal server error |
| 502 | Upstream unavailable. FourA reached its engine but the reply was unusable. Retry. |
| 503 | Service temporarily disabled or at capacity, or Backend service unavailable while an engine restarts |
| 504 | Upstream timeout. The engine didn't finish inside the time budget for this request. Raise timeout_ms or retry. |
Next Steps
- Smart Fetch (Auto): When to let FourA pick the path for you
- Choosing the Right Endpoint: When to pick Single, Proxy, or Browser by hand
- Authentication: Manage your API keys
- Error Handling: Handle errors gracefully
- Site checks: Read the
defensefield and replay a clearance - Why a Proxy Request Ran Out of Tries: Read
attemptReportand act on it - Rate Limits: Understand request limits
- Quick Start: Your first request in 30 seconds