Errors
Every refusal names who said no, because that decides what you do next. Three of them cost nothing, unless your own script ran in the page first: then only our own failures stay free.
| Code | HTTP | Who said no |
|---|---|---|
| VALIDATION_ERROR | 400 | - |
| INVALID_URL | 400 | - |
| SSRF_DENIED | 400 | Our policy said no0cr· denied · not charged |
| UNAUTHORIZED | 401 | - |
| INVALID_TOKEN | 401 | - |
| PLAN_GATE | 402 | - |
| FORBIDDEN | 403 | - |
| AUP_DENIED | 403 | Our policy said no0cr· denied · not charged |
| NOT_FOUND | 404 | - |
| SESSION_NOT_FOUND | 404 | - |
| CONFLICT | 409 | - |
| PAYLOAD_TOO_LARGE | 413 | - |
| UPSTREAM_BLOCKED | 422 | The site said no0cr· blocked · not charged |
| NAV_TIMEOUT | 422 | Time ran out0cr· timed out · not charged |
| PROXY_REFUSED | 422 | - |
| FILE_TOO_LARGE | 422 | - |
| DOCUMENT_TIMEOUT | 422 | - |
| ACTION_FAILED | 422 | - |
| RATE_LIMIT_EXCEEDED | 429 | - |
| QUOTA_EXCEEDED | 429 | - |
| INTERNAL_ERROR | 500 | We broke it0cr· refunded |
| SERVICE_UNAVAILABLE | 503 | We broke it0cr· refunded |
| BILLING_UNAVAILABLE | 503 | We broke it0cr· refunded |
The site said no
An anti-bot wall, a captcha or a hard refusal. Nothing is broken, and nothing was charged unless your own browser.actions script ran in the page first. Retrying the identical call gets the identical answer. Change something: the browser engine, another exit country, your own proxy.
Time ran out
The page wasn't ready before timeoutMs ran out (NAV_TIMEOUT). Nobody refused, and nothing was charged unless your own browser.actions script ran in the page first. Give it more time: raise timeoutMs, or ask for fewer formats.
Our policy said no
A private, loopback or link-local address (on any hop of any redirect), or a host on the acceptable-use list. Retrying cannot help, and nothing was charged unless your own browser.actions script led the page there. Change the URL.
We broke it
A 5xx is ours. It was refunded before you read it, and it is the one refusal worth repeating as-is.
The rest are ours to explain and yours to fix
A bad field, a missing or revoked key, a spent quota, a plan that does not include something. Each says which field or which limit, and a limit that waiting clears carries Retry-After and the moment it resets. A step of yours in browser.actions that did not work answers ACTION_FAILED, names the step and why, and costs nothing. Scripts change that. Once one of yours starts, the call is charged even if a step fails, the site blocks the page, the time runs out, our policy refuses where the script went, your own proxy says no or one of your selectors is too slow for the page it built, because the script may already have read the page. A 5xx is still refunded.
Give your own timeout room above ours
A refusal from us is information: UPSTREAM_BLOCKED means the site said no. If your client's own deadline equals the timeoutMs you send, the two race, and when yours wins you get a transport error instead, which reads like our failure and throws away a true answer that was already on the wire. Keep your deadline comfortably above the one you send us; ten seconds is plenty. This matters most if you refund or retry on your side, because the race turns “the site refused” into “the call broke”, and those two deserve opposite handling.
The shape of a refusal
{
"error": {
"code": "UPSTREAM_BLOCKED",
"message": "example.com refused the request (cloudflare)",
"details": { "status": 403, "reason": "cloudflare", "finalUrl": "https://example.com/" }
}
}
x-request-cost: 0
x-credits-remaining: 987Always this envelope. details carries the field path and the rule it broke, never the value you sent.