# Error codes

> Every error code, its HTTP status, what it costs and what to change.

**TL;DR**: every failure is JSON with an `error.code`. The code is the
contract, the message tells you what to change. A 400 also names the
parameter in `error.field`, and so does a 422. You are never charged for a 4xx or a 5xx.

Every response, good or bad, carries `X-Request-Id`. Quote it if you write to
support. Looking for a fix by symptom instead? See
[Troubleshooting](/start/troubleshooting).

## The body

```json
{
  "error": {
    "code": "bad_parameter",
    "field": "timeout",
    "message": "timeout must be a whole number of seconds, 1 to 90. The default is 60. Seconds, not milliseconds.",
    "report_url": "https://app.megalania.io/report#rid=0199f0a2-7c1e-7000-8000-000000000001"
  }
}
```

`field` is present on a 400, on a 422 `convert_cap` or `unsupported_content`, and on a 503 `unavailable` when one parameter is the cause. `code`, `message` and `report_url` are always there.

`report_url` opens the dashboard's "Report a problem" page with this request id filled in. The id is the only thing in it, after the `#`. Open it, add a short note, and send.

## The table

`none` and `refunded` both cost 0 credits. `refunded` means we held the credits
while the request ran and gave them back.

<!-- generated by make openapi from ErrorCode -->
| HTTP | `error.code` | credits | what to change |
| -- | -- | -- | -- |
| 400 | `bad_parameter` | none | Read `error.field`. That parameter is missing, misspelled, or has a value we do not accept. The message lists the values that work. On checkout: `pack` is missing, or `campaign` is not 1 to 64 of `a-z`, `0-9` and `-`. `network` in `response_types` is this 400 naming `response_types`; send `network=true`. `network` other than `true` or `false`, `network=true` with `engine=http` or with `schema` is this 400 naming `network`. `network_url_globs` without `network=true`, with an empty entry, more than 10 patterns or over 1,024 bytes is this 400 naming `network_url_globs`. `screenshot` other than `viewport` or `full`, or `screenshot` with `engine=http`, or `screenshot` with `schema`, is this 400 naming `screenshot`. `engine=browser` (or a `wait_until` other than `load`, which asks for the browser) is this 400 with a mobile `proxy_tier`, a POST, a `session_id` or an `identity`; the message names the one to drop. A bad `Idempotency-Key`, or the header sent more than once, is this 400 naming `Idempotency-Key`: send it once, 1 to 128 printable ASCII characters. |
| 400 | `bad_request` | none | Checkout and account writes. The body is not one JSON object. Send one JSON object, like `{"pack": "starter"}`. |
| 400 | `bad_signature` | none | Stripe's webhook only. The `Stripe-Signature` header is missing or does not match the body. Not for you to call. |
| 400 | `max_credits_month_too_low` | none | Monitors only, on create, on an edit and on run now. The monitor's `max_credits_month` is below what its checks can cost in a month (the quote's `high`), or this month's spend plus one more check would pass it, so its checks would be skipped. The message has both numbers. Raise `max_credits_month` to at least the number it names, or watch fewer pages or less often. You are not charged. |
| 400 | `max_credits_too_low` | none, or the page only | Every route that can serve this page costs more than your `max_credits`. The message names the lowest price on that site. Raise `max_credits` to at least that, or remove it. This one comes before any attempt, and you are not charged. With `schema`, it can also come after the page: your `max_credits` covered the page but not the most the extraction can cost. Then `error.field` is `max_credits`, the page is charged, the extraction is free (`llm_credits` is 0, `credits` and `X-Credits-Charged` are the page), and the body carries the page as `markdown`. |
| 401 | `bad_session` | none | Dashboard `/api/*` only. There is no session cookie, or the session is over: you logged out, sent nothing for 12 hours, signed in more than 7 days ago, the account was suspended, or you left the account. Sign in again with Google or GitHub. A key in `Authorization` or `api_key` is ignored on `/api/*`. |
| 401 | `bad_token` | none | Send the key in the Authorization header: `Authorization: Bearer <your key>`. The message says which of four it is: no key was sent; the key came in an `X-Api-Key` header; a key is in the URL (`api_key` in the query, refused on every route, even next to the header); or the key is wrong, expired or deleted. A key with an unknown prefix, a key of the wrong length and a deleted key get the same message on purpose. Never charged. |
| 402 | `insufficient_credits` | none | Add credits, or make the request cheaper with `proxy_tier=datacenter`. With `schema`, it can also come after the page: your balance covered the page but not the extraction. Then `error.field` is `schema`, the page is charged, the extraction is free (`llm_credits` is 0, `credits` and `X-Credits-Charged` are the page), and the body carries the page as `markdown`. |
| 402 | `over_plan` | none | You reached a limit of your plan: keys or members (`GET /v1/account` shows the limits under `plan`), or 5 referral codes. Revoke a key with `DELETE /v1/account/keys/{key_id}`, or remove a member with `DELETE /v1/account/members/{person_id}`, then try again. |
| 402 | `payment_failed` | none | `POST /v1/account/subscription` only. The payment for a higher plan failed, so your plan did not change and nothing was charged. Update your card in the billing portal (`POST /v1/account/billing-portal`) and try again. |
| 402 | `spend_cap` | none | Your account's monthly spend cap is reached: this request would take this month's credits past it. Nothing was charged. An owner raises or removes the cap with `PATCH /v1/account` (`spend_cap`), or on the dashboard's Plan and billing page. It resets at 00:00 UTC on the 1st. Batches and monitors stop the same way: a batch item ends `failed` with this error, and a monitor check is skipped for free. |
| 403 | `bad_origin` | none | Dashboard `/api/*` only. A request other than `GET` must come from the dashboard page itself: `Origin` must be the dashboard's address and `Sec-Fetch-Site` must be `same-origin`. A browser sets both. Call `/v1` with a key from anywhere else. |
| 403 | `forbidden_target` | none | The `url` is on a domain we do not fetch, or a subdomain of one. The [acceptable use policy](https://megalania.io/aup/) says what we refuse. `/v1` and batch submit answer this; a batch item on such a domain fails for free. |
| 403 | `monitor_checks_exceeded` | none | Monitors only, on create, on an edit that adds checks to the monitor, on resuming a monitor paused by you or after failed checks (money pauses already count), and on run now when this month's page checks plus the run's pages would pass your plan's monthly checks. Your monitors would run more page checks in 30 days than your plan allows (pages × checks). Paused monitors do not count, except those paused for money, which start again by themselves. The message says how many this monitor adds and how many you use. Check less often, watch fewer pages, or pause another monitor. |
| 403 | `monitor_limit` | none | Monitors only. Your plan has no room for another monitor; paused monitors count too. The message names your plan and its limit. Delete a monitor, or upgrade your plan. |
| 403 | `not_eligible` | none | Referral program only. An account joins after at least one paid purchase that was not refunded or disputed. Buy a pack first. |
| 403 | `notice_required` | none | Making a key, or connecting an AI agent. The person has not accepted the current beta notice. Sign in to the dashboard, read the notice and press I agree. Then try again. |
| 403 | `owner_only` | none | Only an owner can do this: add or change members, read purchases, top up, the referral program. Your role is member. Ask an owner. |
| 403 | `recent_sign_in_required` | none | `POST /api/account/delete` (the dashboard) only. Deleting an account needs a sign-in from the last 10 minutes. Sign in again, then delete. An API key or an AI agent cannot delete an account. |
| 403 | `wrong_scope` | none | The key has the wrong scope. A scrape key (`meg_sk_...`) cannot manage the account: use an account key (`meg_ak_...`) from the Keys page. An account key (`meg_ak_...`) on `/v1` or batches gets: "This is an account key. It can manage the account but cannot scrape. Use a scrape key: it starts with meg_sk_." Only `GET /v1/account` takes both. `DELETE /v1/batches/{id}` (erase) takes only an account key: a scrape key there gets this 403, and can still cancel with `POST /v1/batches/{id}/cancel`. |
| 404 | `not_enrolled` | none | Referral program only. This account has not joined. Join with `POST /v1/account/affiliate`. |
| 404 | `not_found` | none | No batch, monitor, check, key, member or pack with that id or name in your account. Ids of other accounts look the same as ids that do not exist. |
| 404 | `not_subscribed` | none | `POST /v1/account/billing-portal` only. This account has no subscription yet. Subscribe with `POST /v1/account/subscription`. |
| 405 | (no body) | none | `/v1` takes `GET`, `POST` and `QUERY`. The `Allow` header lists them. |
| 409 | `already_member` | none | This email is already a member of this account. Find its `person_id` in `GET /v1/account/members`. |
| 409 | `already_on_plan` | none | `POST /v1/account/subscription` only. You are already on this plan. Pick another plan from `GET /v1/plans`. |
| 409 | `batch_running` | none | `DELETE /v1/batches/{id}` only. The batch is still queued or running, so its data cannot be erased yet. Cancel it with `POST /v1/batches/{id}/cancel`, then erase it once its status is `canceled`. |
| 409 | `checkout_open` | none | `POST /v1/account/subscription` only. A payment page for this account is still open. Finish it, or wait 30 minutes. |
| 409 | `last_owner` | none | This would leave the account with no owner. Make another member an owner first with `PATCH /v1/account/members/{person_id}`. |
| 409 | `monitor_paused` | none | `POST /v1/monitors/{id}/run` only. The monitor is paused. Resume it with `PATCH /v1/monitors/{id}` and `{"paused": false}`, then run it. |
| 409 | `monitor_running` | none | Monitors only. A check is running. Run now waits until it ends; so does an edit of `urls`, `target`, `scope`, `ignore` or the scrape parameters. Try again in a minute. |
| 409 | `monitor_too_soon` | none | `POST /v1/monitors/{id}/run` only. The last check was sooner than your plan's fastest schedule (once an hour), and run now keeps that limit. The message names your plan and the time left; `Retry-After` says it in seconds. |
| 409 | `no_subscription` | none | Packs only. A pack tops up a subscription. Subscribe to a plan first with `POST /v1/account/subscription`. |
| 409 | `notice_changed` | none | Dashboard only. The beta notice changed after the page loaded. Reload the page, read the new notice, and agree again. |
| 409 | `past_due` | none | `POST /v1/account/subscription` only. Your last payment failed. Update your card in the billing portal, then change the plan. |
| 409 | `replayed` | none | You already used this `Idempotency-Key`. Your first call went through and `X-Request-Id` names it. Do not retry. Send a new key for a new request. |
| 413 | `body_too_large` | none | The body is too large. `POST /v1`: 1 MB; a batch: 8 MB, split it; account writes, checkout included: 4 KiB. On `POST /v1`, `body` is the target's own request body, base64, not the page you expect back. |
| 415 | `unsupported_media_type` | none | Every `POST` and `PATCH` takes JSON. Send `Content-Type: application/json`, even with an empty `{}` body. On `/v1` you can also use `GET /v1` with query parameters. |
| 422 | `action_failed` | refunded | An entry in `actions` could not run: no visible element matched its selector in time, or the 30 s for actions ran out. `error.field` is `actions`, and the message names the entry's index (0 is the first). The answer carries no page, cookies or target status. Check the selector, add a `wait_for` before it, or raise `timeout`. Above the [failure cap](#actions-and-the-failure-cap) it is charged, with `X-Billing: failure_cap`. |
| 422 | `bad_extract` | the extraction | The model's answer did not match your `schema`, or the model could not take the schema. The extraction is free; the page is still charged, and the body carries it as `markdown`. Loosen the schema (fewer `required` fields), or add a `prompt`. Each one counts toward 20 failed extractions per account per UTC day; past them, the next extraction is a free 429 `llm_token_cap`. |
| 422 | `blocked` | none | The site blocked us. A block never carries page content: we drop the blocked page, so every block we detect is this 422, free, with `X-Billing: blocked`. The answer keeps the blocked headers: `X-Hops`, `X-Untried`, and `X-Hint`, `X-Block` or `X-Challenge` when they apply. Read `X-Untried` for a route left to try; no `X-Untried` means every route was tried. Above the [failure cap](#actions-and-the-failure-cap) the same 422 is charged, with `X-Billing: failure_cap`. |
| 422 | `convert_cap` | refunded | The page is too large or too complex to convert into the `response_types` you asked for (`markdown`, `text`, `links`, `metadata` or `json`), or it is a document over our caps (64 MiB once unpacked, or more than 500 slides). `error.field` is `response_types`. Ask for `html`, or for fewer `response_types`. When `inspect` is the only output and the page shows no structure, `error.field` is `engine`: the page may build its content with JavaScript, so send `engine=browser`. A site that answers 4xx is not this error: `inspect` answers `200` with the structure as found and a `note`. When `json` is the only output and we had to cut the extract (time, size or step limits) before any field got a value, `error.field` is `extract`: ask for fewer or simpler rules. That answer counts as a failed call. |
| 422 | `document_unreadable` | refunded | The `url` answered with a PDF, Word, PowerPoint or Excel file we cannot read: it holds only scanned images (no text layer), it needs a password, or it is damaged. Nothing was charged. Scanned pages need OCR, which we do not run yet. |
| 422 | `idempotency_mismatch` | none | This `Idempotency-Key` was already used with a different request. On `/v1` and `POST /v1/map`, `X-Request-Id` names the first request; this one did not run and nothing was charged. On `POST /v1/batches` and checkout, send the same body to get the first result back. Otherwise send a new key for a new request. |
| 422 | `target_error` | refunded | The target site answered with a server error (5xx), so there is no page to send. `X-Target-Status` has its status. Try again later; if it keeps failing, the site is down. |
| 422 | `unknown_host` | refunded | Not sent yet: the DNS check behind it is switched off on our servers. Today a host name that does not exist ends as `503 fetch_error`, free, after we tried the other routes. When the check is on, this code means DNS answered that the name in `url` is unknown or has no address: we stop after the first attempt, nothing is charged, and another `proxy_tier` or `country_code` will not help. A DNS server that fails or times out stays `fetch_error`. |
| 422 | `unsupported_content` | refunded | The URL answers with a file we cannot turn into the text `response_types` you asked for: an image, an archive or another binary type. Send the URL of a web page. |
| 422 | `upstream_cap` | refunded | The page pulled more data than one request may use. This also happens when a home-page visit or a client-hints retry we made for you used most of that data first. Fetch a smaller page. |
| 429 | `daily_cost_cap` | none | Your plan's usage limit for today is used up. It resets at 00:00 UTC. Blocks we detect stay free. |
| 429 | `free_failures_spent` | none | Today's free browser and residential retries are used up: failed browser and residential tries may use a share of your plan's daily limit: 20% on beta, 5% on the other plans. Only tries of a request that ends with nothing charged count; a request that ends charged never adds to it. Nothing was charged. Datacenter HTTP still works, and blocks we detect stay free. A request that asks for the browser or residential gets this before any attempt. A request without `engine` or `proxy_tier` still tries datacenter HTTP first, and gets this 429 only if it then needs the browser or residential. A batch item that needs them ends `failed` with this error and nothing charged. `POST /v1/map` answers `200` with the URLs found so far, `truncated: true` and `stopped: { code, message }`. `GET /v1/account` shows `caps.free_failures.used_percent`. `Retry-After` counts down to 00:00 UTC. |
| 429 | `free_tier_cap` | none | Free and beta traffic is paused for all users until the 1st of next month, 00:00 UTC. This is not your account. Blocks we detect stay free. |
| 429 | `llm_global_stop` | none | Schema extraction is paused for every customer until 00:00 UTC: a global safety limit. Nothing was charged; plain fetches and `extract` rules still work. |
| 429 | `llm_token_cap` | none | Two cases: your daily allowance of extraction tokens (per key and per account) is used up, or 20 extractions on this account failed today (`bad_extract`). We stop before the page is fetched. Nothing was charged. It resets at 00:00 UTC; `extract` rules still work. |
| 429 | `over_concurrency` | none | You have too many requests running at once. Wait for one to finish. `Retry-After` says how long. |
| 429 | `period_cost_cap` | none | The usage limit of your current grant is used up. A new grant resets it. Blocks we detect stay free. `X-Usage-Warning` warns you once at 80%. |
| 429 | `rate_limited` | none | Checkout, the account API, batches and monitors (2 calls a second per account, bursts of 10). `POST /v1/monitors/test` also answers this when 3 of your dry tests are running (`Retry-After: 30`). Too many requests to this endpoint right now. Wait the seconds in `Retry-After`, then send the same request again. |
| 429 | `target_rate_limited` | none | The target site is rate limiting you. We hold your requests to that host until `Retry-After` ends, so you do not pay for more 429s. Wait that many seconds. Sending both `engine` and `proxy_tier` skips the hold. |
| 500 | `internal` | refunded | Our bug. Retry once, then send us the `X-Request-Id`. |
| 503 | `busy` | refunded | No capacity right now. Retry after the seconds in `Retry-After`. Many slow pages of one site at once can cause it too: we run a few requests per site at a time, so spread a burst over more sites or send it as a batch. On the account API, batches and monitors nothing was charged: send the same request again in a few seconds. `DELETE /v1/monitors/{id}` and account delete answer this when the stored snapshots and diffs cannot be erased: nothing was deleted, so send it again. |
| 503 | `capacity_spent` | none | Today's residential capacity is used up for everyone. It resets at 00:00 UTC. Leave `proxy_tier` out, or try again tomorrow. |
| 503 | `checkout_closed` | none | Buying credits is not open on this server. Nothing was charged. |
| 503 | `checkout_failed` | none | Checkout only. Stripe did not open the payment page. Nothing was charged. Send the same request again in a minute. |
| 503 | `fetch_error` | refunded | The page could not be fetched. Check the URL. If it is right, retry; if it keeps failing, try another `proxy_tier` or `country_code`. |
| 503 | `llm_unavailable` | none, or the page only | Two cases. Schema extraction is off on this server (`field` is `schema`): we stop before the page is fetched, nothing is charged and there is no page; use `extract` rules instead, retrying will not help. Or the page was fetched but the extraction could not run: fewer than 3 seconds of your `timeout` were left, the extraction hold could not be placed, or the model provider failed or did not answer within 30 seconds. Then the page is charged as delivered and the body carries it as `markdown`; the extraction is free. Retry with a longer `timeout`, or use `extract` rules. |
| 503 | `timeout` | refunded | The fetch did not finish in time. Raise `timeout` (up to 90). |
| 503 | `unavailable` | refunded | Five causes. Screenshots are off on this server (`field` is `screenshot`): leave `screenshot` out. No proxy serves this `proxy_tier` right now (`field` is `proxy_tier`): leave it out, or pick another one. No browser runs right now (`field` is `engine`): try again later, or drop `engine=browser`. Paid proxies are paused for every customer until 00:00 UTC (no `field`): try again after that. On a batch quote, prices are not loaded yet (no `field`): try again after `Retry-After`. |
<!-- end generated -->

Four 400s are worth knowing about:

- **An unknown parameter is a 400.** We do not ignore it. `render=true` copied
  from an old script would otherwise get you a plain fetch, without the
  JavaScript you asked for. `error.field` names the key you sent.
- **A query string we cannot read has no `field`.** Almost always a `url`
  pasted before our parameters. Put `url` last and paste it as it is, or
  percent-encode it.
- **A JSON value of the wrong type names the key.** On `POST /v1` every value
  is a string, `headers` is a list of strings, and `timeout`, `max_credits`
  and `max_age` also take a whole JSON number. `"timeout": 30.5` is a 400
  naming `timeout`; send `"timeout": 30`.
- **A private address in `url` is a 400.** We only fetch public addresses, so
  a literal loopback, private, link-local, unique-local, multicast or broadcast
  address is refused and nothing is charged.
- **A header we set for you is a 400.** `headers` cannot set `Host`,
  `Connection`, `Content-Length`, `Transfer-Encoding`, `TE`, `Trailer`,
  `Upgrade`, `Expect`, `Keep-Alive`, `Via`, `HTTP2-Settings` or any `Proxy-`
  header; the message names the header and nothing is charged.

## The two ways in

`POST /v1` with a JSON object is the canonical call and the one that never
needs encoding. `GET /v1` with query parameters is the paste-a-URL one, and
everything in the next section is about that one only.

`method`, `body` and `content_type` exist only in the JSON envelope. Sending
one of them in a query string is a 400 naming it, so a request you meant as a
POST is never quietly charged as a GET.

## Passing a URL that has its own query string

Paste it. Put our parameters first and `url` last:

```
/v1?engine=http&url=https://example.com/catalogue/page-2.html?sort=price&ref=nb
```

Everything after `url=` that is not one of our parameter names belongs to the
target and reaches it as you sent it. Encoding only the `?` is fine too. Two
cases need a fully percent-encoded `url` instead: the target's own query uses
one of our names (`engine`, `timeout`, ...) or a price word from another
vendor (`render`, `premium`, `country`, ...), or you want to put our
parameters after it. The 400 says so when it happens.

## Retry-After

Sent with `busy` (503), `over_concurrency`, `rate_limited`, `target_rate_limited` and
`free_failures_spent` (429, counting down to 00:00 UTC), `monitor_too_soon` (409), and
`unavailable` on a batch quote (503). It is a
whole number of seconds, from 1 to 86400. Sleep that long, then send the same request
again. The other 429s (`daily_cost_cap`, `period_cost_cap`, `free_tier_cap`,
`llm_token_cap`, `llm_global_stop`) carry no Retry-After: their message names the time
the limit resets. Nothing was charged, so a retry costs you nothing
extra.

## Retrying safely

Send `Idempotency-Key: <your own id>` on any call you might retry. One to 128
printable ASCII characters; a UUID is the usual choice. It is yours alone:
another customer sending the same word does not touch your call. It works on
`GET /v1`, `POST /v1` and `QUERY /v1`.

- The first call with a key runs and is charged as usual.
- Any later call with the same key, inside 24 hours, is a 409 `replayed`. It
  charges nothing and carries the `X-Request-Id` of the first call.
- A replay does not hand you the page again. We keep no bodies, so what a
  replay tells you is that your first call went through and your client should
  stop retrying.
- **Anything we refund releases the key**, so retrying is safe and normal: a
  402, a 429, and every 5xx of ours (`busy`, `timeout`, `fetch_error`,
  `internal`). Send the same request with the same key. You
  were not charged for the attempt that failed, and the retry is charged once.
- A 422 `convert_cap` or `upstream_cap` is refunded too, so it releases the
  key. The same request fails the same way. Change `response_types`, or fetch
  a smaller page, first.
- A **delivered result keeps** the key, and so does a 422 `blocked`. A block
  is free, but it is an answer, so a second call with that key is a
  `replayed`.
- A 503 `busy` where our credit store did not answer stops before the call
  runs, so it is never charged, and it releases the key like any other 5xx.
  If the store is still down when we let go, the key frees itself within a
  minute. Only if the store took the call's reserve and then lost its answer
  can the retry come back 409 `replayed` for up to 24 hours; nothing was
  charged then either, so send a new key.
- Send the header once. Two `Idempotency-Key` headers on one call are a 400
  naming `Idempotency-Key`.
- After 24 hours a key is forgotten. The same key then starts a new request and
  is charged like one, so a key is not permanent protection.
- Without the header nothing changes, and the call costs no extra time.
- `POST /v1/batches` and checkout take the header too. A
  replay there is a 200 with the batch or checkout page the first
  call made. The same key with a different body is a 422
  `idempotency_mismatch`. A replay that races the first call waits up to 2
  seconds for it; if the first call is still not done, the answer is 503
  `busy` with `Retry-After`: ask again with the same key and body.

## 200 is not always what you wanted

A target that answers under 500 is a delivered result, whatever it answered.
You get HTTP 200 from us and the target's own status in `X-Target-Status`. There is no
error body to read, so read the headers.

| what happened | how you see it | credits |
| -- | -- | -- |
| The target answered, with any status under 500 that is not a block, 404 included. | `X-Target-Status` | full price |
| An `http` result that is only a JavaScript shell, and the ladder could not climb to the browser (your plan or `max_credits`). The body is empty. Send `engine=browser` to render it. | `X-Hint: needs_js` | free |
| A shell you asked for with `engine=http`. | `X-Hint: needs_js`, the HTTP body | full price of the route that answered |
| The body passed the 50 MB stop. | The body ends early. The 200 was already sent, so there is no status to change. | full price |

A block is never a `200`. When the target blocks us (401, 403, 407, or an
anti-bot challenge) on every route we try, the answer is a 422 `blocked`
with `X-Billing: blocked`, `X-Hops`, and `X-Untried` when a route is left.
It is free. The one exception is a request with `actions` above the
[failure cap](#actions-and-the-failure-cap): still a 422 `blocked`, but with
`X-Billing: failure_cap`, and charged.

Blocks we detect are free, however often they happen. Your plan's usage limit
still counts what they cost us, so a key that fails all day stops at that
limit with a 429 `daily_cost_cap`. A target that answers 429 is not a block
result: you get our own 429 `target_rate_limited` (see the table above).

### Actions and the failure cap

A request with `actions` can click, type or submit on the target and then fail
on purpose, so free failures there have a cap. We count your requests with
`actions` over a sliding hour, this one included. A request counts as failed
when it ends free: `action_failed`, a block, a timeout or any other error.
When at least 20 of them ran and more than 30% failed, an `action_failed` or a
block is charged the page's price, actions included, and carries
`X-Billing: failure_cap`. Other free errors stay free and only count. It is never charged twice, and bytes above the
included data stay free. The count moves on as the hour slides; requests
without `actions` never count. Schema extraction is never charged for a
failure: after 20 failed extractions in a UTC day, the account's next
extraction is a free 429 `llm_token_cap`, before the page is fetched.

A free answer carries no page content, on `/v1`, map, MCP and in a batch. A
block is a free 422 `blocked`: the headers (`X-Billing`, `X-Block`,
`X-Challenge`, `X-Vendor`, `X-Hops`, `X-Untried`, `X-Target-Status`) say what
happened, and the body is only our JSON error. A target 5xx is a free 422
`target_error` with `X-Target-Status`, never an empty `200`. In a batch, a
free item is `done` with no `result_url`, and its `error` says why: `blocked`,
`target_error` or `needs_js`.

`engine=browser` sends the page straight to a real browser (Chrome, on
datacenter exits). Add `proxy_tier=residential` for residential IPs. It pays
the browser's price: 15 credits on datacenter, 32 on residential, and only
when the page comes back. Bytes above
the included data add a price for each started 128 KiB, up to the byte cap. A browser request reserves that whole worst
case from your balance before it runs, not only the floor; with less balance
or a lower `max_credits`, the page is cut at the bytes you can pay for. You
pay the actual price. Batches price it the same way. When every browser
is busy, the answer is a free 503 `busy` with `Retry-After`. When no browser
runs at all, it is a free 503 `unavailable` naming `engine`.

## Money headers

| header | on | what it says |
| -- | -- | -- |
| `X-Credits-Charged` | every 200 | The charge if the body completes, overage included. It leaves before the body, so it cannot change. If we cut the body (an engine error mid-stream: the multipart closing boundary is missing or the stream ends in an error), the request is refunded in full. If you close the connection after the headers, or read so slowly that the body outlasts your `timeout` after its first bytes reached you, the charge stays. Your usage page is the final record. |
| `X-Credits-Route`, `X-Credits-Output`, `X-Credits-Other` | with `X-Credits-Charged` | The same charge in three parts: reaching the page, what we made of it, and the rest (actions, and a monitor check). They add up to `X-Credits-Charged`. A charged 422 with `X-Billing: failure_cap` carries them too. [Credits](/guides/credits#where-the-credits-went) has the details. |
| `X-Billing` | a 422 `blocked`, and a charged 422 `action_failed` | `blocked`: we detected a block, and you paid nothing. `failure_cap`: a request with `actions` failed or was blocked above the [failure cap](#actions-and-the-failure-cap), and `X-Credits-Charged` says what it cost. |
| `X-Engine` | every 200, and a 422 `blocked` | `http` or `browser`: the engine that produced the result. |
| `X-Route` | every 200, a 422 `blocked`, and a `target_rate_limited` 429 | The engine and proxy tier that answered, such as `http/datacenter`, `http/residential`, `browser/datacenter` or `browser/residential`. |
| `X-Geo` | every answer from a request that ran, blocked or not | The country the request exited from, like `DE`: `country_code` when sent, else the default. |
| `X-Ladder-Truncated` | a 200 where it applies | `true`: your `max_credits` or your balance removed some routes we would have tried. |
| `X-Truncated` | a 200 where it applies | `max_credits` or `byte_cap`: the page reached the most bytes this request may pull. `max_credits` means your `max_credits` set that limit, and the charge stays at or under it. `byte_cap` means the route's or your plan's byte cap, or your balance, set it. The body is the part that fit. Raise it, or remove it, for the whole page. |
| `X-Timing-Target-Ms` | every 200 | Milliseconds from our request to the target's first byte. |
| `X-Identity` | every 200 | `firefox` or `chrome`: the browser the fetch looked like on the wire. Both cost the same. A browser render is always `chrome`. |
| `X-Block` | a 422 `blocked` | `challenge`, `deny` or `rate_limited`: what kind of block we detected. |
| `X-Vendor` | any 200 where we saw one | The anti-bot vendor present on the page, even when it let us through. `X-Challenge` names the one that blocked us. |
| `X-Hops` | every 200 | Every hop we took, measured up to the moment these headers left. Time to stream the body is not in it. It is a JSON array of `{"rung", "identity", "ms", "outcome"}`, like `[{"rung":"h:dc","identity":"chrome","ms":412,"outcome":"blocked"},{"rung":"c:res","identity":"chrome","ms":2900,"outcome":"ok"}]`. `outcome` is `ok`, `blocked`, `needs_js`, or the error code of a try that failed, such as `timeout` or `fetch_error`. The rungs are `h:dc` (HTTP, datacenter), `h:res` (HTTP, residential), `c:dc` (the browser, datacenter) and `c:res` (the browser, residential). |
| `X-Untried` | blocked and `needs_js` results, when a route is left | The rungs your key may use that can serve now and this request did not try, such as `h:res,c:dc`. `X-Hops` lists the ones it tried. No header means every route we have was tried: retrying will not help today. |
| `X-Untried-Reason` | with `X-Untried`, when our budget, not the site, stopped the climb | `budget`: residential routes are unavailable right now, so every residential route was skipped (never charged) and the climb tried the next open one. |
| `X-Hint` | when it applies | `needs_js`: the page is a JavaScript shell, send `engine=browser`. `browser_blocked`: the site blocks our browser too, so we did not try it. |
| `X-Network-Truncated` | a 200 with `network=true`, when it applies | `true`: a cap cut the list of captured responses (200 responses, 8 MiB in all, a URL over 8 KiB, or a body we could not keep). |
| `X-Compact-Truncated` | a 200 with `compact_html`, when it applies | `true`: `compact_html` reached its cap of about 512 KiB and ends at an element boundary. |
| `X-Usage-Warning` | the first 200 after your grant passes 80% of its usage limit | Once per grant: at 100% requests answer 429 `period_cost_cap` until a new grant. |
| `X-Request-Id` | every response | Quote it to support. |

Errors carry no `X-Credits-Charged`, because nothing was charged.

## Safety headers

| header | on | what it says |
| -- | -- | -- |
| `Content-Security-Policy` | every 200, and a 422 `blocked` | `sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'`. The body is the target's page, not ours. If a browser opens it, it runs no script, has no origin and loads nothing from the network: only inline styles and data images show. |
| `X-Content-Type-Options` | every 200, and a 422 `blocked` | `nosniff`. A browser reads the body only as its `Content-Type` says. |
| `Referrer-Policy` | every 200, and a 422 `blocked` | `no-referrer`. A link in the page tells the next site nothing. |

Result and screenshot download links (batches, monitors) download as a file:
they carry `Content-Disposition: attachment`, so a stored page never opens on
our storage host.
