Skip to content

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.

CodeHTTPWho said no
VALIDATION_ERROR400-
INVALID_URL400-
SSRF_DENIED400Our policy said no0cr· denied · not charged
UNAUTHORIZED401-
INVALID_TOKEN401-
PLAN_GATE402-
FORBIDDEN403-
AUP_DENIED403Our policy said no0cr· denied · not charged
NOT_FOUND404-
SESSION_NOT_FOUND404-
CONFLICT409-
PAYLOAD_TOO_LARGE413-
UPSTREAM_BLOCKED422The site said no0cr· blocked · not charged
NAV_TIMEOUT422Time ran out0cr· timed out · not charged
PROXY_REFUSED422-
FILE_TOO_LARGE422-
DOCUMENT_TIMEOUT422-
ACTION_FAILED422-
RATE_LIMIT_EXCEEDED429-
QUOTA_EXCEEDED429-
INTERNAL_ERROR500We broke it0cr· refunded
SERVICE_UNAVAILABLE503We broke it0cr· refunded
BILLING_UNAVAILABLE503We 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

422 Unprocessable Content
{
  "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: 987

Always this envelope. details carries the field path and the rule it broke, never the value you sent.