Docs menu
- Docs
- Reference
- Error codes
Error codes
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.
The body
{
"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.
| 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 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 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 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. |
Four 400s are worth knowing about:
- An unknown parameter is a 400. We do not ignore it.
render=truecopied from an old script would otherwise get you a plain fetch, without the JavaScript you asked for.error.fieldnames the key you sent. - A query string we cannot read has no
field. Almost always aurlpasted before our parameters. Puturllast and paste it as it is, or percent-encode it. - A JSON value of the wrong type names the key. On
POST /v1every value is a string,headersis a list of strings, andtimeout,max_creditsandmax_agealso take a whole JSON number."timeout": 30.5is a 400 namingtimeout; send"timeout": 30. - A private address in
urlis 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.
headerscannot setHost,Connection,Content-Length,Transfer-Encoding,TE,Trailer,Upgrade,Expect,Keep-Alive,Via,HTTP2-Settingsor anyProxy-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 theX-Request-Idof 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_caporupstream_capis refunded too, so it releases the key. The same request fails the same way. Changeresponse_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 areplayed. - A 503
busywhere 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 409replayedfor up to 24 hours; nothing was charged then either, so send a new key. - Send the header once. Two
Idempotency-Keyheaders on one call are a 400 namingIdempotency-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/batchesand 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 422idempotency_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 503busywithRetry-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: 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 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, 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.