# FourA — Complete Documentation and Blog Content > Full content export for AI assistants. Source: https://foura.ai > For the index version, see https://foura.ai/llms.txt ## Pricing Credit-based pricing. You only pay for successful requests. Failed requests, connection errors, and proxy retries are always free. ### Plans | Plan | Price | Credits/month | API keys | Team | Premium traffic | Support | |---|---|---|---|---|---|---| | Free | Free | 3,000 | 1 | 1 | 0.1 GB | Community | | Developer | $29/mo | 75,000 | 3 | 1 | 0.5 GB | Email | | Startup | $99/mo | 450,000 | 10 | 5 | 5 GB | Priority | | Business | $249/mo | 2,250,000 | 25 | 25 | 10 GB | Discord | No credit card needed for Free. Annual billing saves 31 to 33% depending on the plan. Pricing page: https://foura.ai/prices ### Credit Costs | Endpoint | Base | With add-on | Add-on | |---|---|---|---| | Single (one HTTP request) | 1 cr | 2 cr | Unblocker | | Proxy Finder (auto-retries) | 5 cr | 10 cr | Unblocker | | Browser (JavaScript-rendered pages) | 15 cr | 30 cr | a bot defense solved | Auto costs the sum of the sub-calls it made and returns the session that worked, so the next request on that host can replay it through Single. ### Rate Limits | Plan | Single conc | Single RPM | Proxy conc | Proxy RPM | Browser conc | Browser/day | |---|---|---|---|---|---|---| | Free | 3 | 30 | 2 | 10 | 1 | 30 | | Developer | 10 | 120 | 5 | 30 | 1 | 100 | | Startup | 25 | 300 | 15 | 100 | 2 | 300 | | Business | 50 | 600 | 30 | 300 | 3 | 1000 | ### Features by Plan | Feature | Free | Developer | Startup | Business | |---|---|---|---|---| | Unblocker | Yes | Yes | Yes | Yes | | CAPTCHA Solver | Yes | Yes | Yes | Yes | | Browser rendering | Yes | Yes | Yes | Yes | | Premium exits | Yes | Yes | Yes | Yes | | Geo-targeting | No | No | Yes | Yes | | Webhook callbacks | No | No | Soon | Soon | | Scheduled scraping | No | No | Soon | Soon | | SLA | No | No | No | Yes | Every response carries an X-FourA-Credits header with the cost of that call. When a plan ceiling stops a request, the response names the ceiling. ## Documentation ### Api #### API Endpoints Reference URL: https://foura.ai/docs/api/endpoints 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: ```bash 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](https://foura.ai/dashboard). Keys use the `pk_live_` prefix. ## Response Headers Every response from `/api/*` carries 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. Log it on your side. | | `X-FourA-Credits` | integer | Credits spent on this request. Returned on success and on failure (the work was done either way). See [Request Outcomes](/docs/api/outcomes) for which outcomes are billable. | The same request ID keys the request and response payload preview in the Dashboard's [Activity Log](/docs/dashboard/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](/docs/dashboard/playground). Include it when you contact support, and it pinpoints the request in seconds. ```bash $ 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](/docs/api/response-headers) for the full list and usage tips. ## Endpoints > **Using these endpoints via MCP?** The [`@fouradata/mcp` server](/docs/mcp/server) wraps all four endpoints as native MCP tools (`foura_auto`, `foura_single`, `foura_proxy`, `foura_browser`) with the same input shapes plus an `offload_large` opt-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](/docs/guides/choosing-task-type) and the [Smart Fetch guide](/docs/guides/smart-fetch). ## 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. ```json { "error": "Target resolves to a private/reserved IP" } ``` --- ## 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`. | ### Response ```json { "status": 200, "data": "...", "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 or object | Response body. | | `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 a bot challenge was solved during this call. | | `meta.attempts` | number | Sub-attempts made before success. | | `meta.credits` | number | Total credits spent on this call. Matches `X-FourA-Credits`. | | `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 ```bash 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. Each sub-call appears in your [Activity Log](/docs/dashboard/activity-log); the outer `/api/auto/` call doesn't add a separate billable row. - Pass `validate.data.accept` with 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_ms` caps 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. --- ## 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](#pinning-an-exit). | | `browser` | string | No | Chrome | Browser to present: Chrome, Edge, Safari, Firefox, or Tor. See [Browser profiles](#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. ```json { "method": "GET", "url": "https://example.com", "browser": "Firefox", "os": "Windows" } ``` Rules: - Selection requires `unblocker` (on by default). With the unblocker off 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 `request` object of `POST /proxy/`. `GET /api/profiles` returns the full catalogue and needs no API key: ```json { "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. ```json { "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 ```bash 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: ```json { "status": 200, "headers": [{"result": {"version": "HTTP/2", "code": 200, "reason": ""}, "content-type": "text/html", "server": "...", "set-cookie": ["session=abc", "tracker=xyz"]}], "data": "...", "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: ```json { "status": 200, "data": "...", "total_time": 3.61, "defense": { "vendor": "sgcaptcha", "solved": true, "present": ["sgcaptcha"], "ms": 3412, "cookie": "_I_=" } } ``` | 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 [Anti-Bot Defenses](/docs/api/anti-bot-defenses) for every field and the full vendor list. | | `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: ```json { "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 ```bash 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: ```json { "status": 200, "headers": [{"result": {"code": 200}, "content-type": "text/html", "set-cookie": ["a=1", "b=2"]}], "data": "...", "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 only when the request named one. `premium` means a premium exit returned the body; `standard` means the standard pool did. | | `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: ```json { "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](/docs/troubleshooting/proxy-attempt-report). ### 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. ```json { "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 first is a normal success and costs you no premium traffic. **The response tells you what actually served you.** When you name a class, the response carries `exitClass` back: ```json { "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](/docs/api/response-headers#x-foura-exit-class)). **Premium traffic is measured separately.** The bytes of a request served by a premium exit count towards premium bandwidth as well as your total bandwidth; the same bytes, reported twice, never added together. Your [Usage & Limits](/docs/dashboard/quota) 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](/docs/api/rate-limits#plan-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. 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` | Auto-solve common bot challenges (Cloudflare clearance, similar gates) during page load. On by default. Set `false` to render whatever the page returns, including a challenge page, without solving. | | `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](#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: ```json { "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-Language` and 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 [Anti-Bot Defenses](/docs/api/anti-bot-defenses). Send a non-Chromium string (a Firefox User-Agent, say) and it's presented as-is, with no Chromium brand list attached. ### Example ```bash 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: ```json { "status": 200, "headers": {"content-type": "text/html"}, "body": "...", "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. Absent otherwise. Drives the 15 vs 30 credit cost. | | `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 [Anti-Bot Defenses](/docs/api/anti-bot-defenses). | | `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. Two IDs are refused, both 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 bytes count toward the premium traffic on your [Usage & Limits](/docs/dashboard/quota) page as well as toward your total bandwidth. 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, 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`. | | 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)](/docs/guides/smart-fetch): When to let FourA pick the path for you - [Choosing the Right Endpoint](/docs/guides/choosing-task-type): When to pick Single, Proxy, or Browser by hand - [Authentication](/docs/getting-started/authentication): Manage your API keys - [Error Handling](/docs/api/errors): Handle errors gracefully - [Anti-Bot Defenses](/docs/api/anti-bot-defenses): Read the `defense` field and replay a clearance - [Why a Proxy Request Ran Out of Tries](/docs/troubleshooting/proxy-attempt-report): Read `attemptReport` and act on it - [Rate Limits](/docs/api/rate-limits): Understand request limits - [Quick Start](/docs/getting-started/quick-start): Your first request in 30 seconds --- #### API Errors URL: https://foura.ai/docs/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. ```json { "error": "Invalid API key" } ``` Some errors include extra fields like `status`, `service`, `retryAfter`, `current`, or `limits` at the top level: ```json { "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. ```bash 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. ```json { "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: ```json { "error": "Target resolves to a private/reserved IP" } ``` Malformed JSON in the body is refused the same way, before any field is read: ```json { "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. | | `Managed exit: this proxy id cannot be pinned to a request` | The exit is real, 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. | **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. These are `client_error` outcomes: the request never left FourA, so nothing was spent on your behalf. ### 401: Unauthorized Your API key is missing or invalid. Missing key: ```json { "error": "Missing API key. Include X-API-Key header." } ``` Invalid key: ```json { "error": "Invalid API key" } ``` **Fix:** Verify your `X-API-Key` header contains a valid key. Generate a new key from the [Dashboard](https://foura.ai/dashboard) if needed. ### 403: Not in Your Plan The call asked for an endpoint or a parameter your plan doesn't include. The response sets `X-FourA-Limit` and puts the same code in the body under `reason`: ```json { "error": "The browser endpoint is not included in your plan (Free). Upgrade to use it.", "reason": "plan_limit_feature", "documentation": "https://foura.ai/prices" } ``` `reason` is `plan_limit_feature` for an endpoint the plan excludes or for `exitCountries` on a plan without geo targeting, and `plan_limit_premium` for `exitClass: premium` on a plan without premium exits. The `error` string names the endpoint or parameter. A 403 from FourA is never about the target site: the target was never contacted. A 403 the target returned arrives as HTTP 200 with `status: 403` inside the body. **Fix:** Remove the parameter, call an endpoint your plan includes, or upgrade. No `Retry-After` is set, because waiting doesn't change the answer. Nothing was spent: the outcome is `rate_limit`, and only `success` is billed. ### 429: Rate Limited Two different checks answer with 429, and they don't carry the same fields. **Your plan's own limits.** The response sets an `X-FourA-Limit` header naming which limit refused the call and puts the same code in the body under `reason`: ```json { "error": "Concurrency limit reached: your plan allows 50 simultaneous proxy request(s). Retry when an in-flight request finishes.", "reason": "plan_limit_concurrency", "documentation": "https://foura.ai/prices", "limit": 50, "in_flight": 51, "retry_after_seconds": 1 } ``` `reason` is one of `plan_limit_concurrency`, `plan_limit_rate`, `plan_limit_browser_daily`, `plan_limit_credits`, or `plan_limit_bandwidth`. When a wait helps, the wait lives in `retry_after_seconds` and in the `Retry-After` header, never in `retryAfter`. `plan_limit_browser_daily` carries neither, because the allowance comes back at midnight UTC and not in seconds. Nothing was spent: the outcome is `rate_limit`, and only `success` is billed. **The platform's shared allowance.** No `X-FourA-Limit` header, and the wait is in `retryAfter`: ```json { "error": "Rate limit exceeded", "status": 429, "service": "single", "retryAfter": 5, "current": { "concurrency": 12, "rpm": 3000 }, "limits": { "maxConcurrency": 500, "maxRpm": 3000 } } ``` `current` and `limits` describe the service across all traffic, not your account. A refusal here means FourA is busy. **Fix:** Wait whichever of `Retry-After`, `retry_after_seconds`, or `retryAfter` the response carries. On a concurrency or rate limit, cap how many requests you keep open rather than re-sending the refused batch. On a daily or billing-period limit, stop the run. See [Rate Limits](/docs/api/rate-limits) for every field and [Run Requests in Parallel](/docs/how-to/run-requests-in-parallel) for the pattern. ### 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](https://updates.foura.ai/status) 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. ```json { "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. ```json { "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 the platform's concurrency allowance is full. 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. ```json { "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 the platform's real usage. See [Rate Limits](/docs/api/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: ```json { "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: ```json { "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. A plan limit met by one of the sub-calls also comes back as HTTP 200. The body is the refusal itself, with its `reason`, plus `status` and `meta`: ```json { "status": 429, "error": "Credit limit reached: 75,000 of 75,000 credits used this billing period. Upgrade your plan or wait for the reset.", "reason": "plan_limit_credits", "documentation": "https://foura.ai/prices", "used": 75000, "hard_stop": 75000, "retry_after_seconds": 86400, "resets_at": "2026-10-01T00:00:00.000Z", "meta": { "rung": "fail", "solved": false, "attempts": 1, "credits": 0 } } ``` Which limits stop the ladder and which only close a rung is covered in [Smart Fetch (Auto)](/docs/guides/smart-fetch#when-your-plans-limits-meet-the-ladder). ## 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](/docs/api/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 `` 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: ```python import time import requests # Plan limits that no short wait will clear: stop the run instead of retrying. STOP_ON = { "plan_limit_feature", "plan_limit_premium", "plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", } 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 {} request_id = resp.headers.get("X-FourA-Request-Id", "?") limit = resp.headers.get("X-FourA-Limit") if limit in STOP_ON: raise RuntimeError(f"{limit} (request {request_id}): {body.get('error')}") # Plan limits send Retry-After and retry_after_seconds; platform limits send retryAfter. header = resp.headers.get("Retry-After") retry_after = ( int(header) if header and header.isdigit() else body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt ) 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/403/404 won't fix themselves raise RuntimeError(f"{resp.status_code} (request {request_id}): {body.get('error')}") raise RuntimeError(f"Exhausted {max_retries} retries") ``` ## Proxy Failures Carry a Report A `POST /api/proxy/` call that runs out of attempts comes back as HTTP 200 with an error envelope, not as an HTTP error code. The error string is short and always the same shape, so an `attemptReport` object rides beside it with the counts: ```json { "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 } ``` Log `attemptReport.summary` next to the error and you'll know whether the exits were blocked, dead, or delivering pages your own `validate` rules rejected. Field reference and what to do about each count: [Why a Proxy Request Ran Out of Tries](/docs/troubleshooting/proxy-attempt-report). ## Related - [Rate Limits](/docs/api/rate-limits): Plan limits, concurrency, and RPM details - [Run Requests in Parallel](/docs/how-to/run-requests-in-parallel): Staying under your plan's concurrency - [Request Outcomes](/docs/api/outcomes): The seven outcome values explained - [Common Issues](/docs/troubleshooting/common-issues): Symptoms, causes, fixes - [Anti-Bot Defenses](/docs/api/anti-bot-defenses): When the body is a challenge page rather than an error - [Why a Proxy Request Ran Out of Tries](/docs/troubleshooting/proxy-attempt-report): Reading `attemptReport` --- #### Rate Limits URL: https://foura.ai/docs/api/rate-limits Every FourA API request passes three checks before it reaches an engine: the platform's shared allowance for all traffic, the platform's shared allowance for the endpoint you called, and then the limits of your own plan. Each check can refuse a request on its own, and each one answers with a different body. ## The Three Checks, in Order 1. **Global platform limit.** Everything the API host you called is handling at that moment, whichever endpoint the traffic went to. A refusal here reports `"service": "api"`. 2. **Per-endpoint platform limit.** Traffic on the single, proxy, or browser service you called. 3. **Plan limits.** What your own plan allows: which endpoints and parameters it includes, how many requests may run at once per endpoint, how many per minute, how many browser requests per day, and the credits and bandwidth available in the billing period. Checks 1 and 2 count FourA's total traffic, not yours. Read a refusal from either one as "FourA is busy", not as "you sent too much". Check 3 is about your account alone, and nothing else on the platform moves it. `POST /api/auto/` isn't measured on its own. The Single, Proxy and Browser sub-calls it makes for you pass all three checks like any other request, so a parallel batch of auto calls counts against your plan through its sub-calls. ## Plan Limits A plan limit answers with an `X-FourA-Limit` header naming which limit refused the call. The same code is in the body under `reason`, so you can branch on it without reading headers. Every plan-limit body carries `error`, `reason`, and `documentation`; the rest of the fields depend on the limit. | `X-FourA-Limit` | Status | What ran out | |-----------------|--------|--------------| | `plan_limit_feature` | 403 | The endpoint you called, or the `exitCountries` parameter, isn't in your plan | | `plan_limit_premium` | 403 | `exitClass: premium` isn't in your plan | | `plan_limit_concurrency` | 429 | Simultaneous requests on that endpoint | | `plan_limit_rate` | 429 | Requests per minute on that endpoint | | `plan_limit_browser_daily` | 429 | Browser requests for the day | | `plan_limit_credits` | 429 | Billed credits for the billing period | | `plan_limit_bandwidth` | 429 | Bandwidth for the billing period | The numbers behind each limit are your plan's, and the **Limits & Features** tab of [Usage & Limits](/docs/dashboard/quota) lists them next to your live usage. Don't hardcode them: every refusal carries the ceiling that refused it. A refused request spends nothing. The outcome is `rate_limit`, and only `success` is billed. ### Endpoint or parameter not in the plan A 403 with `plan_limit_feature` means the call asked for something the plan doesn't include. The check runs before anything is counted, so the refused call doesn't touch your rate or daily counters. ```json { "error": "The browser endpoint is not included in your plan (Free). Upgrade to use it.", "reason": "plan_limit_feature", "documentation": "https://foura.ai/prices" } ``` The same code and status answer a `POST /api/proxy/` call that sets `exitCountries` on a plan without geo targeting. The `error` string names the parameter: ```json { "error": "Geo targeting (the exitCountries parameter) is not included in your plan (Free). Remove the parameter or upgrade.", "reason": "plan_limit_feature", "documentation": "https://foura.ai/prices" } ``` `plan_limit_premium` is the same shape for `exitClass: premium` on a plan without premium exits. FourA may instead serve such a request from the standard pool and report `exitClass: standard` in the response, so handle both answers. Neither one spends a premium exit. See [exitClass](/docs/api/endpoints#exitclass). Neither 403 sets `Retry-After`. Waiting doesn't change the answer. ### Simultaneous requests Concurrency is counted per endpoint: your plan carries one ceiling for Single, one for Proxy, and one for Browser. The request that goes over comes back as 429 with `Retry-After: 1`: ```json { "error": "Concurrency limit reached: your plan allows 50 simultaneous proxy request(s). Retry when an in-flight request finishes.", "reason": "plan_limit_concurrency", "documentation": "https://foura.ai/prices", "limit": 50, "in_flight": 51, "retry_after_seconds": 1 } ``` `in_flight` counts the refused request too, so it reads at least one more than `limit`. The fix is to bound your own parallelism rather than to retry harder. Answering a 429 by re-sending the same batch immediately produces another 429 for every call in it. See [Run Requests in Parallel](/docs/how-to/run-requests-in-parallel) for a worked pattern. ### Requests per minute Single and Proxy carry a per-minute allowance, measured over a sliding minute. Refused requests count toward that minute too, so hammering during a cooldown doesn't shorten it. ```json { "error": "Rate limit reached: your plan allows 600 single requests per minute. Cool down and retry.", "reason": "plan_limit_rate", "documentation": "https://foura.ai/prices", "limit_per_minute": 600, "current_rate": 613, "retry_after_seconds": 17 } ``` `retry_after_seconds` runs to the end of the current minute. The `Retry-After` header carries the same value. ### Browser requests per day Browser has no per-minute allowance. Its plan limit is a number of browser requests per day, counted from midnight UTC, and the meter counts every browser request admitted, not only successful ones. ```json { "error": "Daily browser limit reached: your plan allows 300 browser requests per day (resets at midnight UTC).", "reason": "plan_limit_browser_daily", "documentation": "https://foura.ai/prices", "limit_per_day": 300, "used_today": 301 } ``` This refusal carries no `retry_after_seconds` and no `Retry-After` header, because the wait is hours rather than seconds. Treat it as a stop and schedule the next run for midnight UTC. ### Credits for the billing period Only billed credits count, which means only successful requests. When the billed total reaches the credits available to you this period, further requests are refused until the period resets or you buy more. ```json { "error": "Credit limit reached: 75,000 of 75,000 credits used this billing period. Upgrade your plan or wait for the reset.", "reason": "plan_limit_credits", "documentation": "https://foura.ai/prices", "used": 75000, "hard_stop": 75000, "retry_after_seconds": 86400, "resets_at": "2026-10-01T00:00:00.000Z" } ``` `hard_stop` is the billed-credit count at which requests stop for this period. Read it from the body rather than computing it: it already includes any credits you bought on top of the plan. ### Bandwidth for the billing period Plans that carry a bandwidth cap refuse requests once the bytes transferred this period reach it. Bought bandwidth counts the same as included bandwidth, and the `error` string quotes what's available to you, not what the plan alone includes. ```json { "error": "Bandwidth limit reached: 50 GB is available to you this billing period.", "reason": "plan_limit_bandwidth", "documentation": "https://foura.ai/prices", "used_bytes": 53687091200, "limit_bytes": 53687091200, "retry_after_seconds": 86400, "resets_at": "2026-10-01T00:00:00.000Z" } ``` On both period limits, `retry_after_seconds` is capped at 24 hours; `resets_at` is the exact instant the period rolls over. ### Plan limit fields | Field | Type | Present on | Description | |-------|------|-----------|-------------| | `error` | string | all | Human-readable message, including the number you're held to | | `reason` | string | all | `plan_limit_` plus the limit name. Same value as the `X-FourA-Limit` header. | | `documentation` | string | all | Link to the plans page | | `retry_after_seconds` | number | concurrency, rate, credits, bandwidth | How long to wait. Same value as the `Retry-After` header. | | `limit` | number | concurrency | Simultaneous requests the plan allows on that endpoint | | `in_flight` | number | concurrency | Requests running on that endpoint for your account, including the refused one | | `limit_per_minute` | number | rate | Requests per minute the plan allows on that endpoint | | `current_rate` | number | rate | Requests counted in the sliding minute, including the refused one | | `limit_per_day` | number | browser daily | Browser requests the plan allows per day | | `used_today` | number | browser daily | Browser requests counted today, including the refused one | | `used` | number | credits | Billed credits so far this period | | `hard_stop` | number | credits | Billed credits at which requests stop this period | | `used_bytes` | number | bandwidth | Bytes transferred so far this period | | `limit_bytes` | number | bandwidth | Bytes available this period | | `resets_at` | string | credits, bandwidth | ISO 8601 timestamp of the period end | Plan limits use `retry_after_seconds`. The platform limits below use `retryAfter`. A retry helper needs to read both, or read the `Retry-After` header, which only plan limits set. ## Platform Limits The platform checks track two things per service and one more across all of them: - **Concurrency**: how many requests FourA is running at the same time. - **RPM**: how many requests FourA has taken in the last 60 seconds. Both counters are shared by everyone using that service. `current` and `limits` in the responses below describe the platform, not your account. If you want your own number, read `in_flight` from a plan-limit response, or open **Usage & Limits** in the [dashboard](https://foura.ai/dashboard). ### 429: RPM Exceeded ```json { "error": "Rate limit exceeded", "status": 429, "service": "single", "retryAfter": 5, "current": { "concurrency": 12, "rpm": 3000 }, "limits": { "maxConcurrency": 500, "maxRpm": 3000 } } ``` The service took its allowed requests for the last minute. Wait `retryAfter` seconds. ### 503: Concurrency Exceeded ```json { "error": "Service at capacity", "status": 503, "service": "proxy", "retryAfter": 2, "current": { "concurrency": 500, "rpm": 1200 }, "limits": { "maxConcurrency": 500, "maxRpm": 3000 } } ``` The service is running as many requests as it's allowed to run at once. This clears in seconds. ## Service Disabled When a service is temporarily taken offline for maintenance, the API returns 503 with a different error message: ```json { "error": "Service disabled", "status": 503, "service": "single", "retryAfter": 60, "current": { "concurrency": 0, "rpm": 0 }, "limits": { "maxConcurrency": 500, "maxRpm": 3000 } } ``` This isn't a rate limit. The service is temporarily unavailable. Check the `retryAfter` value and retry after that many seconds. This typically resolves within minutes. Both 503 shapes carry the same keys, so branch on the `error` string and never on which fields are present. `Service disabled` is maintenance, `Service at capacity` is concurrency. On the maintenance shape, `current.concurrency` and `current.rpm` are always `0`: the request was turned away before anything was measured. ## Platform Limit Fields | Field | Type | Description | |-------|------|-------------| | `error` | string | Human-readable error message | | `status` | number | HTTP status code (429 or 503) | | `service` | string | Which service refused the call: single, proxy, browser, or api | | `retryAfter` | number | Recommended wait time in seconds before retrying | | `current.concurrency` | number | Requests the service was running platform-wide when it refused | | `current.rpm` | number | Requests the service took platform-wide in the last 60 seconds | | `limits.maxConcurrency` | number | The service's platform-wide concurrency allowance | | `limits.maxRpm` | number | The service's platform-wide per-minute allowance | ## Handling Every Refusal With One Helper `Retry-After` is set on the plan limits that are worth waiting for, `retry_after_seconds` is in their bodies, and `retryAfter` is in the platform bodies. Read all three in that order, and stop on the plan limits that no wait will clear: ```python import time import requests # Plan limits that a short wait never clears. STOP_ON = { "plan_limit_feature", "plan_limit_premium", "plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", } def wait_seconds(resp, attempt): header = resp.headers.get("Retry-After") if header and header.isdigit(): return int(header) try: body = resp.json() except ValueError: return 2 ** attempt return body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt def fetch(url, api_key, max_retries=5): for attempt in range(max_retries): resp = requests.post( "https://eu.api.foura.ai/api/single/", headers={"X-API-Key": api_key, "Content-Type": "application/json"}, json={"method": "GET", "url": url}, ) limit = resp.headers.get("X-FourA-Limit") if limit in STOP_ON: raise RuntimeError(f"stopped by plan limit {limit}: {resp.json().get('error')}") if resp.status_code in (429, 503): time.sleep(wait_seconds(resp, attempt)) continue return resp raise RuntimeError("Max retries exceeded") ``` A daily allowance doesn't come back for hours, and a period allowance doesn't come back for days, so treat them as a stop rather than a wait. Read `resets_at` from the body if you want to schedule the next run. ## Tips - Cap the number of requests you keep in flight instead of retrying a refused batch. A retry storm turns one 429 into many. - Read `X-FourA-Limit` first. It tells you in one string whether the limit is yours or the platform's, and no platform refusal sets it. - Don't hardcode the numbers. Every plan-limit response carries the ceiling that refused it, and [Usage & Limits](/docs/dashboard/quota) shows all of them. - `retryAfter` on platform limits is fixed by kind: 2 seconds for concurrency, 5 for RPM, 60 for maintenance. - Match on `error` to tell the two 503s apart. Both shapes carry `current` and `limits`, so a check for "are those fields there?" reads maintenance as a concurrency problem. - A 403 with `X-FourA-Limit` is about your plan, not about the target site. The target never answered. ## Related - [Run Requests in Parallel](/docs/how-to/run-requests-in-parallel): A worked bounded-concurrency pattern - [Usage & Limits](/docs/dashboard/quota): Every plan limit next to your live usage - [API Endpoints](/docs/api/endpoints): Full parameter reference - [Error Handling](/docs/api/errors): All error types and responses - [Response Headers](/docs/api/response-headers): `X-FourA-Limit`, `Retry-After`, and the rest - [Troubleshooting](/docs/troubleshooting/common-issues): Common problems and fixes --- #### Inspect Endpoint URL: https://foura.ai/docs/api/inspect A public diagnostic endpoint that returns your client IP and proxy detection details. Use it to verify whether your outbound traffic is going through a proxy before you start sending requests through FourA. ## Request `GET /inspect` This endpoint is public. No API key, no `X-API-Key` header. ```bash curl https://eu.api.foura.ai/inspect ``` ## Response ```json { "clientIp": "203.0.113.42", "proxyDetection": { "isProxy": false, "proxyType": "none", "anonymityLevel": "none", "confidence": "medium", "detectedHeaders": [], "explanation": "Direct connection (no client-side proxy detected). Note: Elite proxies cannot be ruled out without additional data." }, "proxyChain": { "originalClientIp": "203.0.113.42", "proxyCount": 0, "hasPrivateIps": false }, "timestamp": "2026-04-29T08:07:04.683Z" } ``` ### Top-Level Fields | Field | Type | Description | |-------|------|-------------| | `clientIp` | string | Original client IP (leftmost address in the forwarded chain) | | `proxyDetection` | object | Proxy detection and anonymity analysis | | `proxyChain` | object | Client-side IP chain breakdown | | `timestamp` | string | ISO 8601 timestamp of when the request was inspected | ### `proxyDetection` | Field | Type | Description | |-------|------|-------------| | `isProxy` | boolean | True if client-side proxy headers were detected | | `proxyType` | string | One of: `transparent`, `anonymous`, `distorting`, `elite`, `none` | | `anonymityLevel` | string | One of: `transparent`, `anonymous`, `elite`, `none` | | `confidence` | string | Detection confidence: `high`, `medium`, or `low` | | `detectedHeaders` | string[] | Header names that signaled a proxy (e.g. `via`, `forwarded`) | | `explanation` | string | Plain-English summary of the result | ### `proxyChain` | Field | Type | Description | |-------|------|-------------| | `originalClientIp` | string | Same as top-level `clientIp` | | `proxyChain` | string[] | List of proxy IPs between you and FourA, if any | | `proxyCount` | number | Number of proxies detected in the chain (0 = direct) | | `via` | string[] | Contents of the `Via` header, if present | | `hasPrivateIps` | boolean | True if any private/internal IPs appear in the chain | ## Proxy Types | Type | What it means | |------|---------------| | `transparent` | Forwards your real IP and identifies itself as a proxy | | `anonymous` | Hides your real IP but identifies itself as a proxy | | `distorting` | Sends a fake client IP to the destination | | `elite` | No proxy headers, looks like a direct connection | | `none` | No client-side proxy detected | ## When to Use This - Confirm your outbound traffic isn't passing through a corporate or VPN proxy you didn't expect. - Debug why your real IP shows up in target site logs even when you thought you were anonymous. - Verify a residential or rotating proxy is masking your origin before you send paid requests through FourA. This endpoint inspects your connection to FourA. It does not test FourA's own proxy network. To verify which IP a FourA-routed request comes from, use the [Proxy endpoint](/docs/api/endpoints) and read the response. ## Limits - No authentication required. - Subject to a generous public rate limit. If you call it more than a few times per second from the same IP, expect throttling. - The response reflects only headers and IPs. It cannot detect a perfectly transparent elite proxy that strips all proxy headers. ## Related - [API Endpoints](/docs/api/endpoints): The authenticated request endpoints - [Authentication](/docs/getting-started/authentication): How to authenticate the rest of the API - [Common Issues](/docs/troubleshooting/common-issues): Debug request failures --- #### Request Outcomes URL: https://foura.ai/docs/api/outcomes Every request to the FourA API is classified into exactly one outcome. The outcome is computed once at request time and recorded against your API key. Your dashboard, activity feed, and billing all read the same field. Only `success` is billable. ## The Seven Outcomes | Outcome | Layer | What it means | |---------|-------|--------------| | `success` | n/a | A valid response was delivered. Counts against your billable quota. | | `application_error` | target | The target returned HTTP 200, but the body carried an error field. | | `application_fail` | target | The target returned a non-2xx that your `validate` rules did not accept, or no response at all. | | `client_error` | caller | Your request was rejected before it left FourA. Bad parameters, malformed proxy value, SSRF-guarded URL. | | `rate_limit` | FourA | The request was refused before it ran: by one of your plan's limits (a 403 for an endpoint or parameter the plan doesn't include, a 429 for a spent allowance), or by the platform's shared RPM or concurrency allowance. | | `service_error` | FourA | The backend returned a 5xx, or its body wasn't valid JSON. | | `service_fail` | FourA | Network failure: timeout, connection refused, DNS error, client disconnect. | The layer column tells you who's responsible: - **target** outcomes are about the site you called. Your request reached FourA fine, and FourA reached the target fine. The target itself returned an error. - **caller** outcomes mean your request never had a chance. Fix the request shape. - **FourA** outcomes are on us. Retry, and check the [status page](https://updates.foura.ai/status) if they persist. A target site returning `403` is `application_fail`, not `client_error`. Your call was well-formed. The site just said no. ## Success Is `validate`-Aware Without `validate`, the API marks a request `success` only when the target returns HTTP 200. With `validate`, success follows the rules you declared. If you tell the API that 200 and 403 are both acceptable for a given request, a 403 comes back as `success`. The body still reaches you unchanged. ```bash 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://target.example/feed", "validate": { "status": { "accept": [200, 403] } } }' ``` In this call, a 403 response counts as `success` and bills as one request. A 500 response counts as `application_fail` and is not billed. The same logic applies to `validate.headers` and `validate.data`. Any response the engine accepts against your rules comes back as `success` regardless of HTTP status. ## Billing Implications | Outcome | Billable | Counts toward quota | |---------|----------|--------------------| | `success` | Yes | Yes | | `application_error` | No | No | | `application_fail` | No | No | | `client_error` | No | No | | `rate_limit` | No | No | | `service_error` | No | No | | `service_fail` | No | No | Only requests that delivered the data you asked for are billed. Failures on FourA's side, the target's side, or your own side are all free. ## Reading Outcomes in the Dashboard Every request your API key makes shows up on the [Activity](/docs/dashboard/activity-log) feed with its outcome label. The [Metrics](/docs/dashboard/metrics) and [Overview](/docs/dashboard/overview) pages aggregate the same field for donut charts and timelines. When you filter Activity by outcome, you can also focus on a single product (Single, Proxy, Browser) to see whether a class of failure is specific to one endpoint. ## Retry Heuristics A first-pass retry policy keyed on outcomes: | Outcome | Retry safe? | When | |---------|-------------|------| | `success` | n/a | You have the response. | | `application_error` | Sometimes | Read the target's error body. Some are transient, most aren't. | | `application_fail` | Sometimes | If the target is rate-limiting you, slow down. If it's blocking you, switch to the Proxy or Browser endpoint. | | `client_error` | No | The request will fail again the same way. Fix the input. | | `rate_limit` | Depends | Honor the wait the response gives you: `Retry-After`, `retry_after_seconds`, or `retryAfter`. On `plan_limit_browser_daily`, stop until midnight UTC; on `plan_limit_credits` or `plan_limit_bandwidth`, stop until `resets_at`; on `plan_limit_feature` or `plan_limit_premium`, change the request. | | `service_error` | Yes | Short exponential backoff. | | `service_fail` | Yes | Same as `service_error`. | ## Related - [API Errors](/docs/api/errors): HTTP-level error responses - [Rate Limits](/docs/api/rate-limits): What triggers `rate_limit`, and the two shapes it comes back in - [Metrics](/docs/dashboard/metrics): Where you see outcomes broken down - [Activity Log](/docs/dashboard/activity-log): Per-request outcome history --- #### Response Headers URL: https://foura.ai/docs/api/response-headers Every response from the FourA API includes a small set of custom headers. They're useful for tracing, support, billing reconciliation, and post-hoc analysis. ## Headers FourA Sets | Header | Set on | Description | |--------|--------|-------------| | `X-FourA-Request-Id` | Every `/api/*` response, including errors and 401s | A UUID identifying this request. Log it on your side. | | `X-FourA-Credits` | Every `/api/*` response that reached the backend | Credits spent on this call. Returned on success and on failure (the work was done either way). | | `X-FourA-Limit` | Every `403` or `429` raised by one of your plan's limits | Which limit refused the call: `plan_limit_` followed by `feature`, `premium`, `concurrency`, `rate`, `browser_daily`, `credits`, or `bandwidth`. | | `Retry-After` | Plan-limit `429`s that a wait clears: concurrency, rate, credits, bandwidth | Seconds to wait, as an integer. Matches `retry_after_seconds` in the body. | | `X-FourA-Exit-Class` | Every `/api/proxy/` call that named an `exitClass`, and every Single or Browser call served through a premium exit | `premium` or `standard`: the class of exit that delivered the body. | | `Content-Type` | Every response | Always `application/json` for the envelope. The target's content-type comes back inside the envelope's `headers` field. | ## X-FourA-Request-Id Each call to `POST /api/auto/`, `POST /api/single/`, `POST /api/proxy/`, or `POST /api/browser/` is tagged with a UUID. The header is set even when authentication fails, so you can correlate misconfigured calls too. ```bash 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 X-FourA-Credits: 2 Content-Type: application/json ... ``` ### When to use it - **Support tickets**: include the request ID and we can find the exact call in our records. - **Your own logs**: store it next to your application log line. If a customer complaint says "the data was wrong at 14:32", you can replay the exact request. - **Dashboard tracing**: the same ID appears in the [Activity feed](/docs/dashboard/activity-log) for keys you manage, so you can open the matching row and inspect the captured request and response. ### Example: log on your side ```python import logging import requests log = logging.getLogger(__name__) def fetch(url, api_key): resp = requests.post( "https://eu.api.foura.ai/api/single/", headers={"X-API-Key": api_key, "Content-Type": "application/json"}, json={"method": "GET", "url": url}, ) request_id = resp.headers.get("X-FourA-Request-Id", "no-id") credits = resp.headers.get("X-FourA-Credits", "0") log.info("foura request_id=%s url=%s status=%s credits=%s", request_id, url, resp.status_code, credits) resp.raise_for_status() return resp.json() ``` ```javascript async function fetchPage(url, apiKey) { const resp = await fetch('https://eu.api.foura.ai/api/single/', { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'GET', url }) }); const requestId = resp.headers.get('X-FourA-Request-Id') || 'no-id'; const credits = resp.headers.get('X-FourA-Credits') || '0'; console.log(`foura request_id=${requestId} url=${url} status=${resp.status} credits=${credits}`); return resp.json(); } ``` ## X-FourA-Credits `X-FourA-Credits` reports the credit cost of the call you just made. It's a meter, not a bill: the header reflects what the work spent regardless of outcome. The dashboard's billing layer only counts billable outcomes against your plan (see [Request Outcomes](/docs/api/outcomes) for which outcomes are billable). ### Cost reference | Engine | Base | With `unblocker` | |--------|------|------------------| | Single | 1 | 2 | | Proxy | 5 | 10 | | Browser | 15 | 30 (when a defense was solved) | `/api/auto/` doesn't add a separate billable row. Its credit cost is the sum of the sub-calls it made internally (a single replay on a warm target can finish at 2; a cold solve on a hard site can spend much more). The `X-FourA-Credits` value on the auto response equals `meta.credits` in the body and tracks the full ladder cost. ### Why both a header and a body field? The header is convenient: you can read it before parsing the body, log it next to your request line, or sum it across many calls without JSON parsing. The body's `meta.credits` (Auto) or per-engine metadata (Single, Proxy, Browser dashboards) holds the same number, but readable inside the response envelope. ## X-FourA-Limit `X-FourA-Limit` appears only when one of your plan's limits refused the call. The platform's shared rate limits never set it, so the header is the fastest way to tell "my plan stopped this" from "FourA is busy" without parsing the body. ``` HTTP/1.1 429 Too Many Requests X-FourA-Limit: plan_limit_concurrency Retry-After: 1 X-FourA-Request-Id: 9f1c4e6c-7b2a-4d3e-8a1f-2c9d8e4a3b15 Content-Type: application/json ``` Two of the seven values come with a 403 rather than a 429: `plan_limit_feature` (the endpoint or the `exitCountries` parameter isn't in your plan) and `plan_limit_premium` (`exitClass: premium` isn't in your plan). Neither sets `Retry-After`, because waiting doesn't change the answer. ```python STOP_ON = { "plan_limit_feature", "plan_limit_premium", "plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", } resp = requests.post(url, headers=headers, json=payload) limit = resp.headers.get("X-FourA-Limit") if limit in STOP_ON: stop_the_run(limit) # hours or days away, not seconds elif limit: time.sleep(int(resp.headers.get("Retry-After", 1))) ``` The seven values and the body fields that come with each are in [Rate Limits](/docs/api/rate-limits). ## X-FourA-Exit-Class `X-FourA-Exit-Class` names the class of exit that delivered the body: `premium` when a premium exit did, `standard` when the standard pool did. It appears on a `POST /api/proxy/` response whenever the request named an `exitClass`, where the body carries the same value, and on a Single or Browser response whenever the `proxy` you pinned was a premium exit, where the body has no field for it. ``` HTTP/1.1 200 OK X-FourA-Exit-Class: premium X-FourA-Credits: 2 X-FourA-Request-Id: 2c1d0f9e-4a7b-4c3e-9d2a-8b1f6e5c4d3a Content-Type: application/json ``` Bytes served through a premium exit count toward your premium traffic as well as your total bandwidth. Read the header per request if you reconcile premium traffic yourself; the [Usage & Limits](/docs/dashboard/quota) page shows the same figures for the billing period. What `exitClass` does and when a premium exit is used: [exitClass](/docs/api/endpoints#exitclass). ## Cache Behavior The API does not set `Cache-Control` or `ETag` on responses. Every call hits the backend. If you need caching, add it on your side. ## Target Response Headers The headers the target site returned are not on the FourA API response. They come back inside the JSON envelope as the `headers` field. For the Single and Proxy endpoints, this is an array of per-hop header objects (one entry per redirect step). For the Browser endpoint, it's a flat object of the final response headers. ```json { "status": 200, "headers": [ { "Content-Type": "text/html; charset=utf-8", "Server": "..." } ], "data": "...", "total_time": 0.42 } ``` If you need a specific target header, read it from the envelope's `headers` field, not from the HTTP response of the API call itself. ## Related - [API Endpoints](/docs/api/endpoints): Request and response envelope shapes - [API Errors](/docs/api/errors): How error responses are structured - [Request Outcomes](/docs/api/outcomes): Which outcomes are billable - [Activity Log](/docs/dashboard/activity-log): Per-request history keyed by request ID - [Rate Limits](/docs/api/rate-limits): What each `X-FourA-Limit` value means --- #### Anti-Bot Defenses URL: https://foura.ai/docs/api/anti-bot-defenses When a target runs a bot check on the way to the page you asked for, FourA tells you. Every request that meets one comes back with a field naming the system, whether the check was cleared, and (on a clear) the clearance you can replay so the next call skips it. This page is the reference for those fields. For strategy, see [Handling Anti-Bot Protection](/docs/guides/anti-bot-protection). ## Where the Field Lives | Endpoint | Field | Present when | |----------|-------|--------------| | `POST /api/single/` | `defense` (object) | A bot check was recognised on the response | | `POST /api/proxy/` | `defense` (object) | Same, reported by the attempt that answered | | `POST /api/browser/` | `defenseSolved` (boolean) and `defenses` (object) | A bot check was recognised during the page load | | `POST /api/auto/` | `meta.solved` (boolean) | Always. `true` when a check was cleared somewhere on the ladder. | Absence means nothing was recognised. Don't read a missing `defense` as a failure. Reporting needs `unblocker`, which is on by default. With `unblocker: false` you asked for the page exactly as it came, so Single hands back the challenge untouched and Browser renders it without solving. ## `defense` on Single and Proxy ```json { "status": 200, "data": "...", "total_time": 3.61, "defense": { "vendor": "sgcaptcha", "solved": true, "present": ["sgcaptcha"], "ms": 3412, "hashes": 1048576, "complexity": 20, "cookie": "_I_=" } } ``` | Field | Type | Description | |-------|------|-------------| | `vendor` | string | The system this record is about: the one that was cleared, or the main one met. See the vendor list below. | | `solved` | boolean | `true` means the check was cleared and `data` is the real page. `false` means `data` may be the challenge page. | | `present` | string[] | Every system recognised on this response. Can hold more names than `vendor`, and can hold names nobody clears yet. | | `ms` | number | Milliseconds spent clearing the check. Only on a clear. | | `hashes` | number | How much computational work the challenge asked for. Only on a clear. | | `complexity` | number | The difficulty the challenge declared. Only on a clear, and only where the challenge reports one. | | `answers` | number | How many accepted answers were supplied, for challenges that want several rather than one. Only on a clear. | | `retry` | string | Present when the body came back from a retry rather than from a clear. Today the one value is `refusal-cookies`. See below. | | `cookie` | string | The jar to replay: the clearance a clear earned, or the session a refusal handed out. | `solved: false` is the case worth branching on. FourA never presents an unsolved challenge as content, so the flag is your signal that the body needs escalation rather than parsing. ### `retry: "refusal-cookies"` Some sites don't run a puzzle. They refuse the first request, set cookies on the refusal, and serve the real page to anyone who sends those cookies back. eBay's item pages are the reference case. When that happens, FourA sends them back for you and hands you the page. The response then carries `retry: "refusal-cookies"`: ```json { "status": 200, "data": "...", "defense": { "vendor": "akamai", "solved": false, "present": ["akamai"], "retry": "refusal-cookies", "cookie": "bm_sv=...; dp1=..." } } ``` Read it like this: - **`solved` stays `false`.** Answering a handshake isn't clearing a challenge, and it never changes what the call costs. You're billed for the request you made. - **`data` is real content**, not a challenge page. This is the one case where `solved: false` doesn't mean the body needs escalation, which is why the field exists. - **`cookie` is the session the site handed out.** Replay it the same way you'd replay a clearance and the follow-up pages skip the refusal. - A retry and a clear can both happen on one request. If the retry's answer turned out to be a challenge that FourA can clear, you get `solved: true` with the vendor's own fields **and** `retry: "refusal-cookies"` beside them. `vendor` reads `unknown` when a retry produced the content and no system was recognised on the way. `present` is then an empty array. ## `defenses` on Browser ```json { "status": 200, "body": "...", "userAgent": "Mozilla/5.0...", "defenseSolved": true, "defenses": { "present": ["cloudflare"], "cleared": ["cloudflare"] } } ``` | Field | Type | Description | |-------|------|-------------| | `defenseSolved` | boolean | `true` when a system was met during the load and its clearance is held on the final page. This is the flag that decides whether the call costs 15 or 30 credits. | | `defenses.present` | string[] | Every system recognised at any point during the page load, not just on the final response. A check is something that happened, and by the time the real page arrives the challenge response is long gone. | | `defenses.cleared` | string[] | The systems whose clearance the final page holds. | A name in `present` that never reaches `cleared` is a system FourA can recognise but not yet get past. Those never raise the price of a call. ## Vendors | `vendor` value | The system | |----------------|-----------| | `cloudflare` | Cloudflare challenges and bot management | | `sgcaptcha` | SiteGround's site check | | `datadome` | DataDome | | `perimeterx` | PerimeterX | | `akamai` | Akamai Bot Manager | | `incapsula` | Imperva Incapsula | | `awswaf` | AWS WAF challenge | | `ebay-splashui` | eBay's own challenge | | `hcaptcha` | hCaptcha | | `recaptcha` | reCAPTCHA | | `unknown` | No system was recognised. Only appears alongside `retry`, where the record exists to report the retry rather than a vendor. | ### What Gets Cleared Today | Endpoint | Clears | |----------|--------| | Single, Proxy | `sgcaptcha`, `ebay-splashui`. Both are computational rather than visual, so no browser is involved. | | Browser | `cloudflare`, `sgcaptcha` | Everything else on the list is recognised and reported, and nothing more. That split moves as FourA learns to clear more of them, so read `solved` rather than assuming from this table. Two notes on the edges: - `hcaptcha` and `recaptcha` are also ordinary form widgets. They're only reported when the response actually blocked you (403, 429, or 503), so a checkout page with a captcha field in a form doesn't report a defense. - Being behind Cloudflare isn't a defense. `cloudflare` appears when there's a real challenge or bot-management artifact on the response, not because a site uses Cloudflare. ## Replaying a Clearance `defense.cookie` is the whole point of the field. A clearance is bound to the exit that earned it and the User-Agent that earned it, so replay it through the same pair and the check doesn't run again. ```python import requests API = "https://eu.api.foura.ai" H = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"} # 1) First call pays for the clear. first = requests.post(f"{API}/api/proxy/", headers=H, json={ "maxTries": 5, "request": {"method": "GET", "url": "https://example.com/catalog"}, }).json() defense = first.get("defense", {}) if defense.get("solved"): clearance = defense["cookie"] exit_id = first["proxy"] # 2) Follow-up pages skip the check: same exit, same clearance. for page in range(2, 6): r = requests.post(f"{API}/api/single/", headers=H, json={ "method": "GET", "url": f"https://example.com/catalog?page={page}", "proxy": exit_id, "headers": [["Cookie", clearance]], }).json() print(page, r["status"]) ``` The first call carries the cost of the clear. Every replay is an ordinary request at the ordinary price. Three things break a replay: 1. **A different exit.** Pin the proxy ID the clearing response returned. See [Reuse a Proxy Across Requests](/docs/how-to/reuse-a-proxy). 2. **A different User-Agent.** Browser responses return the `userAgent` they used. Send it back with the cookie. 3. **Expiry.** Clearances have their own lifetimes, set by the target. SiteGround's runs around 30 days for the whole site; a Cloudflare clearance is usually much shorter. Treat a clearance as a cache: when replays start returning challenges again, run one fresh call and take the new one. ## What It Costs A cleared check changes the price on Browser only: | Engine | Base | Cleared defense | |--------|------|-----------------| | Single | 1 (2 with `unblocker`) | No change | | Proxy | 5 (10 with `unblocker`) | No change | | Browser | 15 | 30 | Browser charges 30 only when the solver was on and a system was genuinely cleared. A system that was recognised and not cleared costs 15, the same as a page with no check on it at all. ## Pair It With `validate` `defense` tells you a check was met. `validate` tells FourA what the real page looks like, which is what lets a request fail rather than hand you an interstitial that happens to carry HTTP 200. ```json { "method": "GET", "url": "https://example.com/product/42", "validate": { "data": {"accept": ["Add to cart"], "fail": ["Just a moment"]} } } ``` On `POST /api/auto/`, `validate` is what stops the ladder from accepting a challenge page and calling it done. ## Related - [Handling Anti-Bot Protection](/docs/guides/anti-bot-protection): Which engine to reach for at each protection level - [API Endpoints](/docs/api/endpoints): Request and response reference for all four endpoints - [Reuse a Proxy Across Requests](/docs/how-to/reuse-a-proxy): Pin the exit a clearance is bound to - [Smart Fetch (Auto)](/docs/guides/smart-fetch): How `meta.solved` fits into the ladder - [Response Headers](/docs/api/response-headers): Where the credit cost of a call shows up --- #### Proxy Port URL: https://foura.ai/docs/api/proxy-port Send your own traffic through FourA's exits from any client that accepts a proxy URL: a browser, a download tool, a scraper you already run. The proxy port opens a tunnel to the target you name and copies bytes both ways, so it carries what the JSON API doesn't: streams, large downloads, and whole browser sessions. **Beta.** The proxy port is in beta. Traffic through it isn't billed yet, it isn't covered by monitoring or an uptime commitment, and option names can still change. Tell us what breaks. ## Connecting | Setting | Value | |---------|-------| | Host | `proxy.foura.ai` | | Port | `34004` | | Protocol | HTTP CONNECT with Basic authentication | | Target ports | `443` and `80` | Credentials come from the dashboard. Create a proxy user under **Proxy** in the sidebar and you get a generated username and password. They're separate from your API key on purpose: a proxy handshake travels in the clear on every connection, and a proxy user can be rotated without touching your API integration. See [Proxy Users](/docs/dashboard/proxy). ```bash curl -x http://USERNAME:PASSWORD@proxy.foura.ai:34004 https://example.com ``` ```python import requests proxy = "http://USERNAME:PASSWORD@proxy.foura.ai:34004" r = requests.get("https://example.com", proxies={"https": proxy, "http": proxy}) print(r.status_code) ``` The port speaks CONNECT only. An `https://` target is tunnelled by every client. A plain `http://` target works when your client tunnels it too (curl: `--proxytunnel`); a client that forwards the plain request to the proxy instead gets `405 Method Not Allowed`. ## Options Ride in the Username Everything after the credential is a list of `-key-value` pairs, in any order: ``` USERNAME-network-residential-country-de-session-a1 ``` Values are lowercase words. A dash inside a value starts a new option, so `-city-new-york` reads as two options and is refused. An unknown key is refused too, never dropped: a misspelt `-contry-de` comes back as a 400 that names the supported keys, rather than being served from the wrong country. | Option | Values | What it does | |--------|--------|--------------| | `network` | `shared` (default, alias `pool`), `residential` | Which network the address comes from. Shared is FourA's own exits, included in your plan. Residential is home addresses, drawn from the premium traffic your plan includes plus any you bought. | | `fallback` | `residential`, `off` | Shared network only. Take a residential exit when the shared network can't carry the request. Off unless asked for, and it only fires when your plan includes premium traffic and allowance remains. | | `country` | ISO 3166-1 alpha-2, repeatable | The exit country the target sees. Repeat it for a multi-country scope: `-country-de-country-fr`. | | `session` | 1 to 32 letters, digits, or underscores | Connections sharing a name share an address (sticky). Omit it for a new address per connection (rotating). | | `state` | a word | Region. Residential only. | | `city` | a word | City. Residential only. | | `asn` | a number | Network, by AS number. Residential only. | | `os` | `windows`, `android`, `ios`, `mac` | The kind of device the address belongs to. Residential only. | | `lifetime` | `3` to `1440` | Minutes a sticky residential address is held. Residential only. | Every option except `network`, `fallback`, and `session` accepts `any`, which clears a value saved on the proxy user for this one connection. Three combinations are refused with a 400 rather than partly honoured: - `state`, `city`, `asn`, `os`, or `lifetime` on the shared network. The shared network is chosen by country and nothing finer; add `-network-residential` to target a city. - `-fallback-residential` together with `-network-residential`. A fallback has nothing to do on the network it falls back to. - Finer targeting with a fallback armed. A fallback may or may not fire, so a city it could only sometimes honour is refused. ### Sessions On the shared network, a named session keeps the same exit for as long as you keep using it, on a sliding ten-minute window. If that exit stops working, the session moves to another one rather than failing. A browser driven through the port should always carry a session name: a page load opens dozens of connections, and without a session each one leaves from a different address. On the residential network, a named session holds its address for `lifetime` minutes, or the network's default when you don't set one. ### Saved defaults Targeting saved on a proxy user in the dashboard applies to every connection made with it, so the username can stay short. Anything you put in the username wins for that one connection, and `any` takes a saved value back. A saved residential default spends only when your plan allows it; otherwise the connection is served from the shared network. ### CONNECT headers A client that can add headers to the CONNECT request may send the same options as headers. A header wins over the same option in the username. | Header | Same as | |--------|---------| | `X-Foura-Network` | `-network-` | | `X-Foura-Fallback` | `-fallback-` (`residential` or `off`) | | `X-Foura-Country` | `-country-`, comma-separated for several | | `X-Foura-Session` | `-session-` | | `X-Foura-State` | `-state-` | | `X-Foura-City` | `-city-` | | `X-Foura-Asn` | `-asn-` | | `X-Foura-Lifetime` | `-lifetime-` | ```bash curl -x http://USERNAME:PASSWORD@proxy.foura.ai:34004 \ --proxy-header "X-Foura-Country: de" \ https://example.com ``` ## Responses A tunnel that opens answers `200 Connection established`. Every refusal carries an `X-Foura-Error` header with a one-line reason, so read that header before anything else. | Status | When | |--------|------| | 400 Bad Request | A malformed request, or an option FourA can't honour. `X-Foura-Error` spells out the option and the supported values. | | 403 Forbidden | The target port isn't served, or the target isn't a public internet host. | | 405 Method Not Allowed | A plain request instead of CONNECT. | | 407 Proxy Authentication Required | Missing or wrong credentials. The answer never says which half was wrong. | | 408 Request Timeout | The client connected and sent nothing for 15 seconds. | | 429 Too Many Requests | The proxy user already has 200 tunnels open, or the port is at its capacity or opening rate. | | 502 Bad Gateway | No working exit was found for that target within the selection budget, or FourA's own resolver couldn't look the host up. The two are different messages in `X-Foura-Error`. | | 503 Service Unavailable | The proxy port is switched off. | ## Limits - Up to 200 tunnels open at once per proxy user. - Finding an exit takes up to 45 seconds before the port gives up with a 502. A session that already has an exit usually connects in well under a second. - A tunnel with no bytes in either direction is closed after two idle minutes. - A proxy user you disable or delete in the dashboard stops authenticating within a minute. ## Metering Bytes through the port count toward the bandwidth on your [Usage & Limits](/docs/dashboard/quota) page, and bytes through a residential exit count toward your premium traffic as well. A connection that names `-network-residential` after the premium allowance is spent is served anyway; a fallback or a saved residential default isn't taken in that state, and the shared network serves the connection instead. ## Related - [Proxy Users](/docs/dashboard/proxy): Create proxy users and build a connection string in the dashboard - [Proxy Request](/docs/api/endpoints#proxy-request): The JSON API alternative, with retries and validation built in - [Reuse a Proxy Across Requests](/docs/how-to/reuse-a-proxy): Keeping one exit on the JSON API - [Usage & Limits](/docs/dashboard/quota): Bandwidth and premium traffic against your plan --- ### Dashboard #### Dashboard Overview URL: https://foura.ai/docs/dashboard/overview The FourA Dashboard is where you manage API keys, run test requests, monitor performance, handle billing, and configure your account. This page walks through the main Overview screen and the sidebar sections. ## Accessing the Dashboard Sign in at [foura.ai/dashboard](https://foura.ai/dashboard). You need a FourA account: [sign up here](https://foura.ai) if you don't have one. ## Overview Page The landing page after sign-in shows your headline numbers for the period you pick. ### Summary Cards Five panels across the top: | Panel | What it shows | |------|---------------| | Concurrency | Requests running at the same time right now, plus the average and max for the period | | Requests | Total calls, then Single, Proxy, and Browser, each with its share of the total underneath | | Credits | Billed credits as the value, spent credits underneath, for the total and for each product | | Bytes | Data transferred: In is what you sent, Out is what came back, and Premium is the part of that traffic served through a premium exit. Premium sits inside the two totals and is never added to them. | | Response Time | Average, min, and max across the period | **Billed and spent are different numbers.** Only a successful request is billed, so billed is what you pay for. Spent is every outcome, because a failed request still consumes a proxy or a browser. Reading one under the other's name is how the same account ends up looking like two different accounts on two pages. See [Request Outcomes](/docs/api/outcomes) for which outcomes bill. The three products don't always add up to the total. A small number of calls carry neither, and they're counted in the total and in no product. ### Timeline Charts Below the panels, six timeline charts plot request volume, credit spend, outcome breakdown, response time, bandwidth, and concurrency. Every chart has a toggle in its header that swaps the plot for the underlying table. ### Period and Detail **Period** is how far back you're looking. Eight pills plus Custom: | Pill | Window | |------|--------| | 30M | Last 30 minutes | | 3H | Last 3 hours | | 12H | Last 12 hours | | 24H | Last 24 hours | | 7D | Last 7 days | | 30D | Last 30 days | | 90D | Last 90 days | | 1Y | Last 12 months | **Custom** opens a dialog with four presets, This month, Last month, Last 6 months, and All time, plus a From and To date pair. A date range runs to the end of the day you picked, never past this moment. **Detail** is how wide each bar is: per minute, every 5 minutes, every 30 minutes, hourly, every 6 hours, daily, weekly, or monthly. It defaults to **Auto**, which picks the coarsest step that still puts at least fifteen bars on the chart, so a year reads as a shape instead of 365 hairs. The Auto option names the step it chose. Detail steps that reach further back than FourA keeps that resolution are greyed out rather than hidden: | Resolution | How far back it goes | |-----------|---------------------| | Per-request | 7 days | | 5-minute | 30 days | | Hourly | 12 months | | Daily | The whole history | Pick a period older than a level allows and the finer options grey out. Auto never picks one of them, so a chart is never empty because the data was aggregated away. ### Filters Filters stack and combine, and Period and Detail sit alongside them. | Filter | What it does | |--------|--------------| | Product | Scope to one endpoint: Single, Proxy, or Browser. Auto sub-calls appear under the product that ran them, since Auto isn't a separate billable row. | | Outcome | Isolate one category: Success, Client Error, Service Error, Service Fail, Rate Limited, App Error, or App Fail | | Owner | Everything, Personal, or one of your organizations. Appears only when you belong to at least one. | | Key | Restrict to a single API key | The Concurrency panel and chart don't take the Product or Outcome filter. Both dim while either is on, and the chart says so on its header. ### The Owner Filter The **Owner** dropdown answers "whose traffic am I looking at". It offers **Everything**, **Personal**, and one entry per organization you belong to, and the same control on Metrics, Activity, and API Keys follows your choice, so the four pages never disagree. Usage & Limits and Billing deliberately don't take it. They answer "what bills to me", which for you is your personal keys plus the keys of organizations you own. See [Organizations](/docs/dashboard/teams-and-orgs). ## Sidebar Sections The sidebar links to ten sections, grouped under Dashboard, Access, and Account. ### Overview The page above. Always the starting point after sign-in. ### Playground Test live requests against your real API key without writing any code. Pick Auto, Single, Proxy, or Browser, set a URL, tweak flags and timeouts, and hit Send. Save common configurations as presets, replay any of your last 20 runs from History. Usage counts against the key's quota, the same way a production call would. See the [Playground guide](/docs/dashboard/playground). ### Metrics Outcome distribution donut and a usage table grouped by API key, client IP, or domain. ### Activity A live feed of your most recent API requests. Each entry shows the method, target domain, status, outcome, duration, transfer size, and credits. Use it to debug individual requests or check that a new integration is working. ### API Keys Create, view, and manage the keys your applications use to call the API. Keys use the `pk_live_` prefix. See [Managing API Keys](/docs/dashboard/api-keys). ### Proxy Send your own traffic through FourA's exits from any client that accepts a proxy URL. Create a proxy user, pick a network and a country, and copy the connection string. The section carries a **Beta** badge: traffic through the proxy port isn't billed yet, it isn't covered by monitoring or an uptime commitment, and option names can still change. See [Proxy Users](/docs/dashboard/proxy) and the [Proxy Port](/docs/api/proxy-port) reference. ### Organizations Share keys with colleagues under one owner. Add people by email, set their role, and hand the organization over when it needs to change hands. Usage on an organization key counts against the organization owner's plan. See [Organizations](/docs/dashboard/teams-and-orgs). ### Usage & Limits Your plan, what you've used this billing period, and every limit you're measured against, with a per-key and per-product breakdown. See [Usage & Limits](/docs/dashboard/quota). ### Billing Manage your subscription, buy more credits or traffic, manage payment methods and invoices, and see this period's credits, bandwidth, and premium traffic against your plan. ### Settings Display name and company, password, active sessions, and connected sign-in providers. Email changes aren't available in the dashboard: contact support. ## Tips - Use the **Playground** before writing code. If you don't know which engine a new site needs, start with Auto and let it pick. - Compare billed against spent on the Credits card. A product where the two are far apart is failing often, and the [attempt report](/docs/troubleshooting/proxy-attempt-report) says why. - Keep your API keys secure. Disable any key immediately if you suspect it's been exposed. ## Next Steps - [Authentication](/docs/getting-started/authentication): API key creation and management details - [Quick Start](/docs/getting-started/quick-start): Make your first request from the CLI - [Playground](/docs/dashboard/playground): Run live requests from the dashboard - [Usage & Limits](/docs/dashboard/quota): Track credits against your plan - [API Endpoints](/docs/api/endpoints): Use the API directly --- #### Creating a Task URL: https://foura.ai/docs/dashboard/creating-a-task This guide walks you through creating an API key and sending your first request using the FourA Dashboard. ## Step 1: Open the Dashboard Navigate to the [Dashboard](https://foura.ai/dashboard) and sign in with your FourA account. ## Step 2: Create an API Key Go to the **API Keys** page and click **Create Key**. Give the key a descriptive name (e.g., "production-scraper" or "dev-testing"). The confirmation dialog shows the plain key with a copy button. If you miss it, click the eye icon on the key's row later to reveal it again. Legacy keys created before reveal shipped can't be brought back; regenerate them once to switch over. Full flow: [Managing API Keys](/docs/dashboard/api-keys). Your key looks like this: `pk_live_a1b2c3d4e5f6...` ## Step 3: Choose an Endpoint FourA has four endpoints for different scenarios: ### Auto (`POST /api/auto/`) The smart default. Pass a URL plus a `validate` rule and FourA picks the cheapest path that works: direct request, rotating proxy, or full browser. Best when you're targeting a new site and don't yet know what it needs. ### Single (`POST /api/single/`) Sends a fast HTTP request. Best for static HTML pages and API endpoints. Response time: typically under 2 seconds. ### Browser (`POST /api/browser/`) Runs a Chrome browser instance to render JavaScript. Best for SPAs, lazy-loaded content, and pages that require JS execution. Response time: 2 to 10 seconds. ### Proxy (`POST /api/proxy/`) Routes the request through rotating proxies with automatic retry. Best for sites with bot detection or geo-restricted content. ## Step 4: Send Your First Request Open a terminal and run: ```bash 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"}' ``` ## Step 5: Read the Response ```json { "status": 200, "headers": [ { "result": { "version": "HTTP/2", "code": 200, "reason": "" }, "content-type": "text/html; charset=UTF-8", "content-length": "1256" } ], "data": "...", "total_time": 0.45 } ``` The `headers` field is an array of objects, one per redirect hop. Each entry has a `result` with the status line plus every response header the target returned. Key fields: - **status**: HTTP status code from the target site - **data**: the response body (HTML, JSON, or raw text) - **total_time**: request duration in seconds ## Step 6: Try a Browser Request If the target page uses JavaScript to render content, use the browser endpoint instead: ```bash 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/products", "timeout_ms": 15000}' ``` The browser response uses `body` instead of `data`: ```json { "status": 200, "headers": {"content-type": "text/html"}, "body": "..." } ``` ## Common Issues | Problem | Solution | |---------|----------| | Not sure which engine to use | Start with the auto endpoint. It picks the right path and remembers what worked. | | Empty content | Switch from single to browser endpoint: the page likely needs JS rendering | | Captcha in response | Switch to the proxy endpoint for automatic IP rotation | | Timeout | Increase `timeout_ms` or verify the URL is correct | | Lost your key | Click the eye icon on the key's row in **API Keys** to reveal it, or regenerate if the key is legacy | ## Next Steps - [Dashboard Overview](/docs/dashboard/overview): Full dashboard walkthrough - [Playground](/docs/dashboard/playground): Test requests in the dashboard before writing code - [Smart Fetch (Auto)](/docs/guides/smart-fetch): Deep dive on the auto endpoint - [Choosing the Right Endpoint](/docs/guides/choosing-task-type): Detailed comparison - [Common Issues](/docs/troubleshooting/common-issues): Solve problems quickly --- #### Managing API Keys URL: https://foura.ai/docs/dashboard/api-keys The API Keys page lets you create, view, and manage the keys your applications use to call the FourA API. ## Viewing Your Keys The page shows every key you have access to: your personal keys, and every key owned by an organization you belong to. Membership is what grants access, so a plain member sees the organization's keys too. | Column | What it shows | |--------|---------------| | Name | Display name and, if you set one, the description | | Owner | `Personal`, or the organization's name | | Key Prefix | Masked prefix (`pk_live_••••••••`) with a Reveal button, or a lock icon on a legacy key | | Status | Active or Inactive | | Created | When the key was created | | Last Used | When the key last made a request | Search by name or description, filter to Active or Inactive, and sort by any header. Sort direction cycles descending, ascending, then off. ### The Owner Filter If you belong to at least one organization, an **Owner** dropdown appears next to the search box: **Everything**, **Personal**, or one entry per organization. It narrows the list to keys with that owner, and the same control on Overview, Metrics, and Activity follows your choice, so the four pages always agree about whose traffic you're looking at. ## Creating a Key Click **Create Key** and fill in: | Field | Required | Description | |-------|----------|-------------| | Name | Yes | A descriptive label, for example "production-scraper" or "staging-test" | | Description | No | Notes about what this key is used for | | Owner | Yes if you're in an organization | Personal, or any organization you belong to. The picker only appears when you have at least one. | Any member of an organization can create a key it owns, whatever their role. That's the point of the setting: usage on an organization key bills the organization's owner, so a developer doesn't need a personal key that quietly bills them instead of the company. The full key is shown once right after creation in a copy-ready dialog. You can bring it back later with **Reveal**. Keys use the `pk_live_` prefix and look like `pk_live_a1b2c3d4e5f6...`. You'll get an email confirming the key was created. ## Revealing a Key Click the eye icon next to a key's masked prefix to see its full secret again. The dashboard asks you to confirm, then opens a dialog with the plain key, a copy button, and a short countdown. After the countdown, the dialog wipes the secret from the page. Every reveal is recorded in the audit log, because it exposes a live credential. Anyone the key is shared with can reveal it: its personal owner, or any member of the organization that owns it. ### Legacy Keys Keys created before the reveal feature shipped show a lock icon instead of the eye and can't be revealed. Only their hash was stored, so the original secret is unrecoverable. To switch a legacy key over, open the actions menu and choose **Regenerate**. The new secret is revealable from then on. ## What Each Role May Do On a personal key you can do everything. On an organization key, three levels apply: | Action | Member | Admin | Owner | |--------|--------|-------|-------| | Use the key and reveal it | Yes | Yes | Yes | | Rename it, edit the description | Yes | Yes | Yes | | Enable or disable it | No | Yes | Yes | | Regenerate the secret | No | Yes | Yes | | Delete it | No | Yes | Yes | | Edit the IP allowlist or expiry | No | Yes | Yes | | Move it out of the organization | No | No | Yes | Buttons you can't use are disabled with a note saying why, and the API refuses the same actions, so an integration can't route around the interface. The split follows what each action costs the people sharing the key. Renaming is harmless. Regenerating or disabling stops every colleague's integration the instant it happens. Moving a key out takes it away from the company and puts its usage on the taker's own plan, so that one is the owner's alone. ## Editing a Key Click **Edit** to update the display name and description. Owners and admins also get the IP allowlist and expiry here. Ownership isn't changed on this dialog: use **Transfer**. ## Activating and Deactivating Use **Disable** or **Enable** on the row. Deactivating a key blocks every API request that uses it, and you can reactivate it at any time. The change takes effect within seconds. ## Regenerating a Key If you suspect a key has been exposed, open the actions menu and choose **Regenerate**. This creates a new secret for the same key ID. The old secret stops working immediately. Name, owner, and every other setting stay the same. The new secret stays revealable after the dialog closes, so you don't have to copy it perfectly on the first try. ## Transferring a Key **Transfer** in the actions menu moves a key between personal and organization ownership without regenerating the secret. The key ID, its metrics, and its activity history are preserved. Only the owner changes, and with it who the usage bills to. | Move | Who can do it | |------|---------------| | Personal key into an organization | Anyone who belongs to that organization | | Organization key out, to yourself | The organization's owner | | Between two organizations | The owner of the organization the key is leaving | A key that moves into an organization joins its default "Everyone" team, which is what makes it visible to the members. A key that moves out leaves every team it was in. ## Deleting a Key Open the actions menu and choose **Delete**. Deleting a key you've used before soft-deletes it: the key stops authenticating, and its Activity and Metrics history stay intact. Keys that have never been used are removed outright. You'll get an email when a key is deleted. ## Organization Keys and Billing Usage on an organization-owned key counts against the **organization owner's** plan, whoever fired the request. The owner's [Usage & Limits](/docs/dashboard/quota) page counts those keys alongside their personal ones and splits the two. ## Related - [Authentication](/docs/getting-started/authentication): How API keys work with the API - [Organizations](/docs/dashboard/teams-and-orgs): Roles, members, and shared access - [Usage & Limits](/docs/dashboard/quota): Which keys count against your plan - [Dashboard Overview](/docs/dashboard/overview): All dashboard sections --- #### Metrics URL: https://foura.ai/docs/dashboard/metrics The Metrics page (sidebar > Metrics) gives you a deeper analytical view of your API usage. It shows your overall outcome distribution and breaks down traffic by API key, client IP, or target domain. For summary cards and timeline charts (Concurrency, Requests, Credits, Bytes, Response Time), use the [Dashboard Overview](/docs/dashboard/overview) page. ## Filters Four controls sit above the page: Owner, Key, Period, and Detail. They're the same controls as on Overview and they share their state, so switching between the two pages never changes what you're looking at. ### Owner Filter **Everything**, **Personal**, or one of your organizations. It only appears once you belong to at least one. Picking an owner also rebuilds the Key dropdown, so a key from the previous owner can't survive the switch and silently return nothing. ### Key Filter Scope the page to a single key. Only keys you have access to appear: your personal keys and the keys of organizations you belong to. ### Period Eight pills, 30M, 3H, 12H, 24H, 7D, 30D, 90D, and 1Y, plus **Custom** for a calendar month, the last 6 months, all time, or an exact pair of dates. ### Detail How wide each bar is, from per minute up to monthly. It defaults to **Auto**, which picks the coarsest step that still leaves the chart readable. Steps reaching further back than FourA keeps that resolution grey out. Full explanation in [Dashboard Overview](/docs/dashboard/overview). ## Outcome Distribution A donut chart at the top shows the success and error breakdown for the selected period. Hover any slice for the exact request count and percentage. Use it to check whether a drop in success rate lines up with one outcome category. ## Outcome Types Every API request is classified into exactly one outcome. Only `success` is billed. | Outcome | Layer | Meaning | |---------|-------|---------| | `success` | n/a | The request delivered a valid response. With no `validate` rules, that means HTTP 200. With `validate` rules, any response your rules accepted, whatever the status. | | `application_error` | target | The target returned HTTP 200, but the body carried an error field. | | `application_fail` | target | The target returned a non-2xx your `validate` rules didn't accept, or no response at all. | | `client_error` | caller | Your request was rejected before it left FourA: bad parameters, a malformed proxy value, or a URL that resolves to a private or reserved IP. | | `rate_limit` | FourA | The request was rejected by your RPM or concurrency limit. See [Rate Limits](/docs/api/rate-limits). | | `service_error` | FourA | The backend returned a 5xx, or replied with a body we couldn't parse. | | `service_fail` | FourA | A network failure: timeout, connection refused, DNS error, client disconnect. | The layer column tells you who's responsible. `target` means the site you called, `caller` means your request was bad, `FourA` means we couldn't process it. If you use `validate.status.accept` to allow specific non-200 codes (for example `[200, 403]`), those come back as `success` instead of `application_fail`. The classification follows the engine's verdict on your rules, not the raw HTTP code. For the full taxonomy and how it maps to billing, see [Request Outcomes](/docs/api/outcomes). ## Usage Table Below the donut, a usage table breaks down your traffic with three view tabs: | Tab | Groups data by | |-----|----------------| | API Key | Each key you have access to | | Client IP | Source addresses making the requests | | Domain | Target domains in your requests | Scope chips on the right change the columns: | Scope | What it shows | |-------|---------------| | Bandwidth | Request count, bytes in, bytes out, and the premium share of those bytes (traffic served through a premium exit, inside the totals rather than added to them) | | Response Time | Request count, min, average, max latency | | Concurrency | Request count plus concurrent request counts (API Key view only) | | Outcomes | Request count plus a per-outcome breakdown | | Budget | Request count and credits spent, by product | The Budget scope reads the credits metric the API reports on every request (see [Response Headers](/docs/api/response-headers)). Those sums are the raw metered spend. Billing counts only the `success` outcome against your plan, and the Overview Credits card shows both side by side. Click a column header to sort. Sort state persists per table across page loads. Ctrl or Cmd click a header to reset. ## Related - [Dashboard Overview](/docs/dashboard/overview): Summary cards, timeline charts, Period and Detail - [Request Outcomes](/docs/api/outcomes): The seven outcome values explained in detail - [Response Headers](/docs/api/response-headers): Where credits come from - [Organizations](/docs/dashboard/teams-and-orgs): What the Owner filter covers - [API Errors](/docs/api/errors): How errors come back over the wire --- #### Activity Log URL: https://foura.ai/docs/dashboard/activity-log The Activity Log shows your most recent API requests in real time. Use it to debug individual requests, check response codes, and verify that your integration is working. ## What You'll See The log displays requests made within the last hour, with the most recent at the top. Nine columns carry the whole row: | Column | What it shows | |-------|-------------| | Time | When the request was made | | Key | Which API key was used | | Request | The call itself: a colored product dot (single, proxy, or browser), the HTTP method, and the target domain. Sorts by domain. | | Status | Two values in one cell, separated by a slash: the HTTP status of your call to FourA, then the status the target returned. A dash means the target never answered. | | Outcome | Request classification (Success, Client Error, Rate Limited, and so on) | | Duration | Total response time in milliseconds | | Bytes | Transfer both ways in one cell: down arrow for the request payload, up arrow for the response. A row served through a premium exit carries a **premium** mark on this cell; those bytes count toward your premium traffic. | | Credits | Credits spent on the call (see [Response Headers](/docs/api/response-headers)) | | Client IP | The address that made the request | Click any column header to sort the table by that column; direction cycles descending, ascending, then off. Ctrl or Cmd click resets to the default (newest first). ## Filtering Four controls narrow the log. ### By Owner If you belong to at least one organization, an **Owner** dropdown appears first: **Everything**, **Personal**, or one of your organizations. Changing it rebuilds the key dropdown next to it, so a key belonging to the previous owner can't survive the switch and silently return nothing. The same control on Overview, Metrics, and API Keys follows your choice. See [Organizations](/docs/dashboard/teams-and-orgs). ### By API Key Use the API key dropdown to show requests from one key. Only keys you have access to appear: your personal keys and the keys of organizations you belong to. ### By Product Filter to one endpoint with the **single / proxy / browser** selector. Useful when you want to debug browser-only failures separately from single requests. ### By Limit The activity log defaults to 50 entries. Use the limit selector to change how many entries are shown: | Limit | Notes | |-------|-------| | 10 | Quick scan | | 50 | Default | | 100 | Extended view | | 200 | Maximum | All entries are from the last hour. For historical data, use the [Metrics](/docs/dashboard/metrics) section, which aggregates data over days and weeks. ## Opening a Request Click any row to open a detail panel with the full request and response payload preview. The panel opens on a meta grid built from the row itself, so it stays useful even after the stored payload has aged out: timestamp, key, HTTP status, app status, outcome, duration, credits, proxy, and the request's `X-FourA-Request-Id`. Below the grid, one pane shows at a time, selected by tab: | Tab | What it shows | |-----|---------------| | Request | Pretty-printed JSON of the exact body sent | | Response headers | The headers the target returned, with a count on the tab | | Response body | The stored body preview, with truncated and binary badges where they apply | | Other fields | Anything else the engine returned beyond status, timings, proxy, and headers. Hidden when there's nothing extra. | A **Copy** button copies whichever pane is open. Payloads are kept for 24 hours, capped at the last 200 per API key. Rows older than that show the row only, without a payload. ### Body Pane Messages The body pane uses different placeholder text depending on what happened: | Message | What it means | |---------|---------------| | `(no body — the request failed: )` | The request errored before the target returned a body | | `(no body captured for this request)` | Payload aged out or wasn't stored | | `(empty body — the server returned 0 bytes)` | The target returned a real empty response | ## Open in Playground The detail dialog has an **Open in Playground** button. Click it to load both the archived request and the archived response into the [Playground](/docs/dashboard/playground) form. From there you can tweak parameters and replay against the live API, or just inspect what came back without re-running the request. The button is disabled for non-replayable payloads (oversized request stubs and non-API routes), with a hint explaining why. ## Using the Request ID Every API response carries an `X-Foura-Request-Id` header. Log it on your side, and you can paste it into a support ticket to point at the exact request in the Activity Log. The ID is the same one used in this dialog and matches the `X-Foura-Request-Id` returned by the API. See [Response Headers](/docs/api/response-headers) for details. ## Related - [Metrics and Analytics](/docs/dashboard/metrics): Aggregated performance data over longer periods - [Playground](/docs/dashboard/playground): Replay requests from Activity - [Response Headers](/docs/api/response-headers): Where the request ID and credits come from - [API Endpoints](/docs/api/endpoints): Request and response shapes - [Organizations](/docs/dashboard/teams-and-orgs): What the Owner filter covers - [Troubleshooting](/docs/troubleshooting/common-issues): Common problems and solutions --- #### Usage & Limits URL: https://foura.ai/docs/dashboard/quota The Usage & Limits page in the dashboard shows your plan, what you've used this billing period, and every limit you're measured against. ## Where to Find It Sign in to the [dashboard](https://foura.ai/dashboard) and open **Usage & Limits** in the sidebar. ## Overview Tab - **Billing period context**: the exact start and end dates of the window you're in. Paid plans reset on your billing date each month. Annual plans get monthly cycles anchored to activation. The Free plan anchors to your signup date. - **Credits this period**: five stats side by side. Billed, Spent, Included, Remaining, and Overage. Only successful requests are billed and count against your plan. Spent is the total resource use including failed requests, which we never charge for. A usage bar under the stats colors amber at 80% and red at or over the included allowance. - **Per-product breakdown**: cards for Single, Proxy, and Browser show the credits and requests each product used this period, plus a last-used timestamp. ## API Keys Tab A sortable table of every key that answers to your plan, with billed credits, spent credits, requests, share of usage, and last used. Keys of organizations you own count against your plan too. Deleting a key never deletes its usage history. This page ignores the dashboard's **Owner** filter on purpose. It answers "what bills to me", which is your personal keys plus the keys of organizations you own. A member of somebody else's organization never sees the owner's quota. See [Organizations](/docs/dashboard/teams-and-orgs). ## Limits & Features Tab - **Plan limits**: one row per limit with your current value, what's included, and the status. | Row | What it counts | |-----|----------------| | API keys | Active keys you answer for, split into personal and organization | | Team members | The largest organization you own, its owner included. An organization you only belong to counts against its owner, not you. | | Organizations you own | How many you own, and whether your plan covers having one at all | | Credits (this period) | Billed credits against what's available: what the plan includes plus any credits you bought, shown apart (`75,000 (50,000 incl + 25,000 bought)`) | | Bandwidth | Data transferred against your allowance, when your plan carries a cap. Bought bandwidth counts the same as included bandwidth. | | Of which premium | The share of that bandwidth served through a premium exit, against the premium traffic your plan includes plus any you bought. Running out reads **spent**, never over: requests keep working on the standard pool and the response says `exitClass: standard`. A plan with none included reads **top-up only**. | | Per product | Requests per minute, concurrency, and browser requests per day for Single, Proxy, and Browser, each with its live counter next to the plan's number. A product your plan excludes reads **Not in plan**. | - **Plan features**: the capabilities included in your plan, such as geo targeting (`exitCountries`) and premium exits (`exitClass`). Features not in your plan show as excluded so you can see what an upgrade would unlock. The numbers in this tab are the ones the API holds you to. A refusal from any of them arrives with an `X-FourA-Limit` header naming the row: see [Rate Limits](/docs/api/rate-limits). ## Freshness Numbers on this page come from the billing pipeline and refresh every few seconds. If a request finished a moment ago, give the page one refresh to catch up. Live counters for concurrency, per-minute rate, and browser-per-day sit next to your caps and update in near real time. ## Related - [Billing & Subscriptions](/docs/dashboard/billing): Manage your plan, buy more credits or traffic, payment methods, and invoices - [Metrics](/docs/dashboard/metrics): Timelines, outcome breakdown, and per-key charts - [Activity Log](/docs/dashboard/activity-log): The last 200 requests per key with payloads - [Organizations](/docs/dashboard/teams-and-orgs): Members, roles, and who the usage bills to - [Rate Limits](/docs/api/rate-limits): What triggers rate-limit responses --- #### Billing & Subscriptions URL: https://foura.ai/docs/dashboard/billing The Billing section is where you manage your subscription, buy more of what your plan meters, and keep payment methods, invoices, and standing invoice details in order. Open it from the **Billing** link in the dashboard sidebar. The page has four tabs: Plan & Usage, Billing Details, Payment Methods, and Invoices. ## Plan & Usage ### Current plan The first card names the plan you're on and its status (Active, Trialing, Past due, Cancelled, and so on), with the renewal date underneath. A plan an operator placed with a start date in the future reads **Starts** with that date instead of **Renews**. The buttons on the card depend on where you are: | Button | When it appears | |--------|-----------------| | Change plan | You're on an active plan. Opens the plan picker, with proration. | | Cancel | You're on an active plan. | | Resume | You cancelled, and the period hasn't ended yet. | | Confirm *plan* | FourA placed you on a paid plan you haven't paid for yet. Opens checkout so you pick how to pay. | | Start *plan* | FourA offered you a plan that carries no charge, such as a trial. Starts it without a card. | | Your custom plan: *plan* | A custom plan is assigned to your account and you're not on it yet. | ### Usage cards Three cards below the plan, one per thing a plan meters. Each shows what you've used this billing period against what's available to you, which is what the plan includes plus anything you bought on top, and each carries a **Buy** button when that kind of top-up is on sale for your plan. | Card | What it measures | |------|------------------| | Credits used this period | Billed credits: successful requests only. Spent credits from failed requests are never charged. | | Bandwidth used this period | All traffic, standard and premium. A plan without a bandwidth cap reads **No cap on this plan**. | | Premium traffic this period | The part of your bandwidth served through a premium exit, which you get by sending `exitClass: premium`. When the included amount runs out, requests keep working on the standard pool and the response says `exitClass: standard`. | Usage bars colour amber at 80% and red at or over the available amount. The Plan & Usage tab links to [Usage & Limits](/docs/dashboard/quota) for the per-product and per-key breakdown and every plan limit. ### Buying more Click **Buy credits**, **Buy bandwidth**, or **Buy premium** on the matching usage card. The dialog prices the amount you enter on the server, shows subtotal, VAT and total, and charges a saved card or a new one entered right there. The allowance is yours a moment after the charge goes through. What you buy lasts twelve months as a balance. Each period your plan's own allowance is used first, and only what you go over draws on the balance. A **Top-ups** box under the cards lists what you hold and until when. The box and the Buy buttons stay away entirely when your plan sells no top-ups, rather than showing refusals. ## Billing Details The **Billing Details** tab holds the standing invoice profile applied to every future invoice: - **Invoice email**: where PDF invoices are sent. Defaults to your account email. - **Company name**: appears on invoices when set. - **VAT / Tax ID**: EU VAT IDs are validated live against the VIES database. A green **Valid VAT** badge shows below the field once the check clears; an amber note shows if the format is right but VIES can't confirm the number. - **Company representative**: the person who represents the company. Bulgarian invoices print it as MOL in the recipient box. Leave it empty if it doesn't apply to you. - **Address**: street, city, postal code, and country. Country is a real dropdown. Saved details apply to the next invoice you generate. An invoice that's already been issued keeps the details it was frozen with, with one exception: you can correct an invoice until the end of the month it was issued in. See [Correcting an Invoice](#correcting-an-invoice). ## Payment Methods Manage the cards on file for your account. - **View cards**: every saved payment method with card brand, last four digits, and expiry. - **Add a card**: click **Add Payment Method** to open the card entry form. The card is stored with the payment provider. - **Set default**: click the menu on any card and choose **Set as Default**. The default card is used for all future charges. - **Remove a card**: click the menu on any card and choose **Delete**. You can't delete your only payment method while you have an active subscription. ## Invoices The Invoices tab lists every invoice on your account. | Column | Description | |--------|-------------| | Invoice | The invoice number | | Date | When the invoice was issued | | Amount | Total charged | | Status | Paid, open, or void | | PDF | Download link for the invoice PDF | | Actions | Refund (paid invoices only), and **Update details** while the invoice is still correctable | ### Correcting an Invoice An invoice freezes your billing details at the moment it's issued. If you fix a typo in your company name or add a VAT ID afterwards, the invoice that already went out still shows the old values. You can correct it, but only until the end of the month it was issued in. - If your current billing profile differs from what the invoice froze, the row shows an **Update details** button. - If the invoice is still inside its window but there's nothing to change, the row reads **Editable until** with the date instead, so you can see the window exists before you need it. - After that date the invoice is final. Click **Update details** and a dialog lists exactly which fields change and what they change from. The invoice number, the dates, and the amounts never move. Confirming is your explicit consent to the correction, and it's recorded. If your change adds or edits a tax ID, the dialog says so: a tax ID can change how VAT applies to a sale, so that case gets a human look and we get in touch if anything needs adjusting. ### Requesting a Refund Click **Refund** on any paid invoice and confirm the dialog. Full refunds are processed immediately against the card that paid the invoice; the amount lands back on your statement within your card issuer's usual window. Partial refunds aren't a self-service action; contact support if you need one. ## Upgrading Your Plan Click **Upgrade** from the Plan & Usage tab (or from the upgrade prompt on the Overview page) to open the plan picker. The picker includes: - **Public plans tab**: side-by-side comparison of the standard plans with features and pricing. - **Your custom plans tab**: only appears if support has assigned one or more custom plans to your account. - **Billing interval toggle**: monthly or annual. The annual badge shows the exact saving for the plan in view, computed from the two prices. A plan sold at one interval only shows that interval and no toggle. - **Card entry**: if you don't have a card on file, you enter one here. - **Proration preview**: when switching between paid plans, you see the exact prorated amount (or credit) before confirming. If you don't have an active subscription, **Upgrade Plan** opens the same picker so you can subscribe directly. After you confirm, your new plan takes effect immediately. ## Confirming a Plan FourA Placed You On Support can place your account on a plan from an agreed date, so the plan's limits apply before any money moves. Until you pay, the Plan & Usage card shows **Confirm *plan*** with the monthly price. Click it and the checkout page reads **Confirm your plan**: your plan is already active, and you choose how to pay for it. Pick monthly or annual, enter a card if you have none on file, and the button charges the full period (**Confirm and pay**). A placed plan that carries no charge needs no confirmation. It's simply yours. ## Changing Plans To switch between paid plans, go to **Billing > Plan & Usage** and click **Change plan**. You see the same picker as the upgrade flow, including proration. Each plan is priced at the interval it's sold at, and a plan sold at one interval only says so (**sold annually only**) rather than quoting a price it doesn't have. - **Upgrades** apply immediately with a prorated charge on today's card. - **Downgrades** apply at the end of the current billing period. If your bank asks for 3D Secure on the proration charge, the dialog hands you the authentication step before anything changes. The new plan lands once you finish it, not before. Close that step without completing it and you stay on your current plan. ## Canceling Click **Cancel** on the Plan & Usage tab. You can optionally provide a reason. After canceling: - Your plan stays active until the end of the current billing period. - You won't be charged again. - You can resume before the period ends by clicking **Resume** on the Plan & Usage tab. ## Related - [Dashboard Overview](/docs/dashboard/overview): Real-time stats and navigation - [Usage & Limits](/docs/dashboard/quota): Every plan limit vs your current usage - [Rate Limits](/docs/api/rate-limits): What the API answers when an allowance runs out - [API Keys](/docs/dashboard/api-keys): Manage your keys - [Metrics](/docs/dashboard/metrics): Request analytics and usage breakdown --- #### Organizations URL: https://foura.ai/docs/dashboard/teams-and-orgs An organization groups API keys under one owner so several people can share them. Everything an organization's keys spend counts against the plan of whoever owns the organization, not against the person who fired the request. ## Where to Find It Sign in to the [dashboard](https://foura.ai/dashboard) and open **Organizations** in the sidebar. Every organization you belong to is a row: | Column | What it shows | |--------|---------------| | Organization | The name. Click it, or **Open**, to go to the organization's own page. | | Your role | Owner, Admin, or Member | | Members | How many people are in it, the owner included | | Keys | How many API keys the organization owns | | Created | When it was created | ## Creating an Organization Click **New organization**, enter a name between 2 and 60 characters, and confirm. You become the owner. The name has to contain at least one letter or digit, and you can't have two organizations with the same name. Creating an organization needs a plan that covers more than one person. The **Organizations you own** row on [Usage & Limits](/docs/dashboard/quota) shows whether yours does. ## Roles There are three, and every organization has exactly one owner. | Role | What it can do | |------|----------------| | Member | Create and use the organization's API keys, rename them, and see the organization's members and teams | | Admin | Everything a member can do, plus add and remove people, change their roles, manage every organization key, and rename the organization | | Owner | Everything an admin can do, plus hand the organization over to someone else. The owner carries the plan every organization key bills against. | The owner can't be removed, can't leave, and can't be demoted. Ownership moves one way: by transfer. Being in the organization is what grants access to its keys. A member with no admin rights can still create a key the company pays for, which is the point: a developer writing collectors shouldn't need a personal key that quietly bills them instead of the company. ## The Organization Page **Open** on any row takes you to that organization's page. Five cards sit at the top: Members (with the owner, admin, and member split underneath), Your role, Owner, API keys, and Teams. Below them, two tabs: - **Members**: the people in the organization, plus the controls your role gives you. - **Teams**: the teams you belong to inside this organization, read-only. Owners and admins also get a **Rename** button in the page header. ## Adding People Owners and admins get an **Add a member** card above the Members table. Enter an email address, pick **Member** or **Admin**, and submit. What happens next depends on the address, and the dashboard tells you which it was: - The address already has a FourA account: they join immediately and get an email naming the organization, who added them, and what their role can do. - The address doesn't: FourA emails them an invitation and they join the moment they sign in with that address. Nothing to click, and the invitation works whether they sign up fresh or already had an account they'd never opened. Invitations expire after 14 days. Re-adding the same address refreshes the pending invitation instead of stacking a second one. The owner gets an email whenever somebody joins, because the new person spends the owner's credits. Nobody gets an email confirming their own action. ### Pending Invitations While at least one invitation is open, an **Invitations** table sits above Members, visible to owners and admins. It lists the address, the role waiting for them, who invited them, and when. Expired invitations stay listed and are marked, so you can see why nobody arrived. Use the action on the row to withdraw one. ## Changing a Role Owners and admins change a member between **Member** and **Admin** with the dropdown in the Role column. Three rules: - You can't change your own role. Ask another admin, or leave. - The owner's role isn't editable here. Use **Make owner** instead. - Every change asks first and says what the person will be able to do afterwards. ## Removing Someone, and Leaving Owners and admins get **Remove** on every row but their own and the owner's. Your own row carries **Leave** instead, whatever your role. Removing someone takes them out of the organization's teams too, so they immediately stop seeing its keys. It doesn't touch the keys themselves: an organization key belongs to the organization, not to whoever made it. The owner can neither be removed nor leave. Hand the organization over first. ## Handing the Organization Over Only the owner can do this. On the Members tab, click **Make owner** on the person taking over and confirm. The organization moves to their plan: from that moment every request its keys make counts against their credits, their limits, and their invoice. The outgoing owner becomes an admin and keeps everything an admin can do. ## Which Keys You See Every key an organization owns is visible to everyone in the organization. What differs is what you may do with it: | Action | Member | Admin | Owner | |--------|--------|-------|-------| | Use the key, reveal it, create new organization keys | Yes | Yes | Yes | | Rename it, edit its description | Yes | Yes | Yes | | Regenerate, disable, delete, edit the IP allowlist or expiry | No | Yes | Yes | | Move the key out of the organization | No | No | Yes | Renaming a key is harmless. Regenerating or disabling one stops every colleague using it the instant it happens, which is why those sit with admins. Moving a key out takes the asset out of the company and moves its usage onto the taker's own plan, which is why that sits with the owner alone. See [Managing API Keys](/docs/dashboard/api-keys) for the full walkthrough. ## The Owner Filter Once you belong to an organization, an **Owner** dropdown appears above Overview, Metrics, Activity, and API Keys. It has three kinds of option: **Everything**, **Personal**, and one entry per organization. The choice sticks between visits and applies to every one of those four pages. [Usage & Limits](/docs/dashboard/quota) and [Billing](/docs/dashboard/billing) deliberately don't take the filter. They answer "what bills to me", which is a different question from "what can I see": a member never sees the owner's quota. ## Billing Attribution Every request made with an organization-owned key bills the **organization owner**, whoever fired it. That's the whole reason to move a key into an organization: it changes who the usage counts against for credits, plan limits, and invoices. The owner's [Usage & Limits](/docs/dashboard/quota) page counts organization keys alongside their personal ones, and splits the two so it's clear where the spend came from. ## Teams Every organization gets a default team called **Everyone**. New keys are filed into it automatically and everyone who joins the organization is added to it, which is what makes membership give access to the keys. The Teams tab lists the teams you belong to, with your role in each and their member and key counts. Creating and editing teams isn't in the dashboard yet. ## Related - [Managing API Keys](/docs/dashboard/api-keys): Create, transfer, and manage keys inside an organization - [Usage & Limits](/docs/dashboard/quota): Team members, organizations, and what your plan allows - [Billing & Subscriptions](/docs/dashboard/billing): How the owner's plan covers organization-key usage - [Dashboard Overview](/docs/dashboard/overview): All dashboard sections --- #### Account Settings URL: https://foura.ai/docs/dashboard/account-settings The Settings page lets you manage your profile, security, connected services, and active sessions. Open it from the sidebar in the dashboard. ## Profile Update your display name and company. Changes sync across the platform. | Field | Description | |-------|-------------| | Name | Your display name (shown in the dashboard and emails) | | Company | Your company or organization name | Email address changes aren't supported through the dashboard. Contact support if you need to update your email. ## Security ### Changing Your Password If you signed up with email and password, you can change it here. Enter your current password and a new one (minimum 8 characters). ### Setting a Password (OAuth Users) If you signed up with Google or another OAuth provider, you can set a password to enable email-based login as an alternative. No current password is required the first time. ### Password Reset Click **Forgot Password** to receive a password reset link by email. ## Connected Services View and manage OAuth connections (like Google). You can disconnect a service to remove it as a sign-in option. Make sure you have a password set before disconnecting your only OAuth provider. ## Sessions View all your active sessions across devices. Each session shows: | Field | Description | |-------|-------------| | Browser | Browser name and version | | Operating System | OS name and version | | Device | Device type | | IP Address | Where the session originated | | Last Used | When the session was last active | | Expires | When the session automatically ends | ### Revoking Sessions Revoke individual sessions or all sessions at once. Revoking a session signs that device out immediately. Your current session is marked so you don't accidentally sign yourself out. ## Related - [Dashboard Overview](/docs/dashboard/overview): All dashboard sections - [Authentication](/docs/getting-started/authentication): How API keys and authentication work --- #### Playground URL: https://foura.ai/docs/dashboard/playground The Playground (sidebar > Playground) lets you run live API requests against your real key without writing any code. It's the fastest way to try a new target site, debug a tricky response, or compare Auto, Single, Proxy, and Browser side by side. Open it at [foura.ai/dashboard#playground](https://foura.ai/dashboard#playground). ## What It Does One form. Four engines. Real traffic. - **Auto**: smart fetch. You pass a URL plus a `validate` rule and FourA picks the cheapest path that works. - **Single**: direct HTTP fetch with realistic browser-like wire characteristics - **Proxy**: managed rotating proxy fetch, optionally scoped to target-visible countries - **Browser**: opens the URL in a Chrome browser instance for JS-rendered sites Requests run against the API key you pick at the top of the page. Usage counts against that key's quota the same way a production call would, so don't burn through your plan in testing. ## Picking a Key The API key dropdown shows every active key within your scope: personal keys, org keys you administer, and team-shared keys you can access. Pick the one you want the request to bill against. If you don't have any active keys yet, an inline prompt links you to the **API Keys** page to create one. ## Choosing a Mode A top **Mode** row toggles between **Auto** and the manual engines. When Auto is selected, the form switches to the minimal Auto surface (URL plus `validate` plus a few knobs). When you switch off Auto, three pills appear: **Single**, **Proxy**, **Browser**. Switching pills swaps which fields are visible and which engine the request hits. The current selection is preserved when you reload the page. | Mode | When to use it | |------|----------------| | Auto | New target or mixed-protection site. Auto picks the cheapest path and remembers what works. | | Single | Fast HTTP fetch. Best first pick for a known host. | | Proxy | Same fetch with automatic proxy rotation. Set `exitCountries` when you need a target-visible country. | | Browser | Loads the page in a Chrome browser instance. Use when the data appears only after JavaScript runs. | ## Building the Request ### URL Row The top row holds the HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), the target URL, and the **Send** button. Single, Proxy, and Auto honor every method. Browser ignores the method (Chrome always issues GET for navigation) and the body. ### Request Tabs Below the URL row, five tabs let you fill in everything else: | Tab | What it controls | |-----|------------------| | UI | Form fields for timeouts, redirects, flags, proxy, browser-specific options, and validate rules | | Body | Free-form body for POST / PUT / PATCH requests | | Headers | Custom request headers as key-value pairs | | Cookies | Cookies to send with the request | | Raw | The exact JSON payload that will be sent, editable directly | Whatever you change in UI / Body / Headers / Cookies is reflected in Raw. Editing Raw works too, with the other tabs updating to match. A red dot appears on any tab or collapsible section that holds a value different from the engine's defaults, so you can spot at a glance what you've customized. ### UI Pane Sections The UI tab groups settings into collapsible sections. Empty fields fall back to the engine's schema default. Sections that don't apply to the current Mode are hidden. - **Timeouts**: `timeout_ms`, `connect_timeout_ms`, `accept_timeout_ms`, `server_response_timeout_ms`, `dns_cache_timeout_sec`. Auto exposes only `timeout_ms` (the total budget). - **Redirects**: toggle and set `followRedirects` (0-20). Single, Proxy, Auto. Browser follows redirects on its own. - **Flags**: `unblocker` for Single, Proxy, and Browser (unblocker on Browser triggers the auto defense solver); `tryJsonData` and `returnBuffer` for Single and Proxy. Auto exposes `forceProxy` and `returnSession` instead. - **Proxy**: pick a specific proxy ID for Single or Browser, or set `maxTries`, the Proxy outer timeout, `exitCountries`, `exitClass`, and `ignoreProxies` for the Proxy engine. Auto also exposes `ignoreProxies`. The `exitClass` select has three states: unset sends no field at all, `standard` says the request must never escalate, and `premium` lets it escalate to a premium exit when the standard pool is struggling. Unset and `standard` are different requests, so leave the select empty unless you mean one of the two. Premium needs a plan that includes premium exits: see [exitClass](/docs/api/endpoints#exitclass). - **Browser profile**: three cascading dropdowns, **os**, **browser**, and **version**, listing what FourA can actually present. They show up in Single and Proxy mode. Leave them empty for the newest Chrome. Each select narrows the other two, so a combination that resolves to nothing never appears. The section needs Web Unblocker on: with it off no browser headers are sent, a profile would only half-apply, and the API refuses the request instead. - **Browser**: browser-only options such as `checkStatus` and `checkText`. - **Validate**: `validate.status` (status codes), `validate.headers` (header key-value rules), and `validate.data` (body accept / fail substrings, `|`-separated alternatives) accept / fail rules. Available for every Mode, including Auto. ### Exit Country Scoping (Proxy Mode) The **exitCountries** field on Proxy accepts a comma-separated list of two-letter target-visible country codes (`CZ, GB`). Values are trimmed, uppercased, and deduplicated on submit. Selection is a strict allowlist: proxies with unknown exits are excluded and the request never falls back to another country. If the current pool has no match, the response returns `code: "no_eligible_proxy"` with the requested scope echoed back in `details.exitCountries`. Preserve the scope and retry later. When a proxy call succeeds under scoping, the response strip shows `exit ` next to the proxy ID so you can verify the served country matches what you asked for. ### Toolbar Reset The **Reset** button on the toolbar (next to **History** and **Saved**) clears the playground back to a clean slate. Because it's destructive, it opens a confirm dialog that lists exactly what will be wiped: all three product forms (Single, Proxy, Browser), any saved cookies in the jar, any carried proxies, and the current response. Saved presets and the selected API key are kept. Click **Reset everything** to confirm; anything else cancels. ## Sending and Canceling Click **Send** to fire the request. The right column flips to a loading state with a spinner and a **Cancel** button while the call is in flight. Click Cancel (or hit the button again on mobile) to abort. A canceled request restores the idle placeholder with "Request canceled." instead of rendering an error. The response card switches to the result the moment the request completes (or fails). Auto runs can take longer than the manual engines because the ladder may climb several rungs on a cold target. ## Reading the Response The response column mirrors the request layout with its own tabs: | Tab | What it shows | |-----|---------------| | Body | Parsed body. Switches between JSON, HTML, and Text views depending on what came back. | | Headers | Response headers, one per line. | | Cookies | Cookies returned by the target, in both parsed (host-grouped) and raw (Set-Cookie text) views. The parsed view shows an `HO` badge on host-only cookies; domain cookies are unmarked. | | Raw | The full JSON envelope returned by the API. | A meta strip above the tabs shows the upstream HTTP status, the total time, the proxy ID that handled the call, and (for a scoped Proxy call) the two-letter `exit `. For Auto runs, the strip also shows which ladder rung delivered the response, how many sub-attempts were made, and the credits spent. ### What the Call Needed A sentence under the meta strip says what carried the page, in words. For an Auto run it names the rung (a session FourA already had for the host, a plain request, a rotating proxy, a real browser, or a browser first and then a cheap replay), whether a challenge was solved, how many attempts it took, and what it cost. When one of your plan's limits refused the call, the sentence says so first: "Stopped by your plan, not by the site", followed by which limit (today's browser requests used up, too many requests in flight, this period's credits spent, and so on) and a link to [Usage & Limits](/docs/dashboard/quota). The line is built from the `X-FourA-Limit` code the API returned, so a hard page that fails tells you whether the site stopped it or the plan did. ### Carry Values Between Runs After any run that returned reusable session data, a small **Carry** control on the response toolbar shows what's available: - **Auto** runs offer the full `session` triple (`proxy`, `cookies`, `userAgent`). - **Browser** runs offer the response `userAgent`, plus the proxy ID if one was used. - **Proxy** runs offer the returned proxy ID, the browser profile when the rotation picked one you didn't ask for, and the `exitClass` that served the call, so a premium answer can be sent straight back. Click **Carry** and pick where to apply each value in one click: `userAgent` becomes a `User-Agent` header on Single or Proxy, and the proxy ID drops into the `proxy` field on Single or Browser. Fields that receive a carried value show the "modified" red dot so you can see what changed. A carried **browser profile** fills the three os, browser, and version selects and turns Web Unblocker on, the same rule that applies when you pick a profile by hand. It's offered only once the profile catalogue has loaded, since the form is three selects and not an id field. The profile is the one value that says the request which *worked* was not the request you typed: Proxy reports `profile` only when it moved to a browser family you didn't ask for. Replay without it and you replay the version that failed. See [Why a Proxy Request Ran Out of Tries](/docs/troubleshooting/proxy-attempt-report). ### Expand to Full Screen The expand icon on the response toolbar lifts the response card out of the split layout and into a full-screen overlay. Use it for deep JSON trees, long Set-Cookie dumps, or wide HTML bodies where the half-width column gets cramped. The page itself stops scrolling while the overlay is open. Click the icon again (or press Escape) to collapse. ## The curl Reproducer Below the response, a curl block shows the exact command line equivalent of the request you just built. Copy it to repro the request from a terminal, share it with a teammate, or paste it into a bug report. For revealable keys, a **Reveal key** button next to the snippet drops the real plain-text key straight into the curl so you can copy-and-run as is. Click again to hide. Legacy keys (created before the reveal feature shipped) keep a `PASTE_PLAINTEXT_FOR_` placeholder; regenerate the key from the **API Keys** page to make it revealable. The reveal is audit-logged on the server every time, and the plain key only lives in memory for the current page session. ## Saving Presets If you find yourself reconfiguring the same target repeatedly, save it. Click **Save** on the request tabs row to store the current configuration as a named preset. Open **Saved** in the toolbar to browse, rename, or delete your presets. Click any preset to load it back into the form. | Preset field | What it stores | |--------------|----------------| | Name | A short label (up to 100 characters) | | Description | Optional notes (up to 500 characters) | | Endpoint | Which engine the preset is for (auto / single / proxy / browser) | | Config | The full request payload, including UI fields, headers, cookies, and body | Presets are scoped to your user account and aren't shared with team members. ## Replaying from History Every request you run is logged. Open **History** in the toolbar to see your last 20 runs, sorted newest first. Each row shows the endpoint, target URL, status, and time. Click **Replay** on any row to load that request back into the form, then **Send** to run it again. History is automatically scoped to your account: you only see your own runs. ## Opening from Activity The [Activity Log](/docs/dashboard/activity-log) detail dialog has an **Open in Playground** button. Click it and the Playground loads with both the archived request and the archived response. The form fills in from the stored payload, and the response card shows what the API returned at that moment with an "archived" badge on the proxy meta strip ("archived