Reference
Errors
Errors are JSON with a stable code, a human-readable message and an HTTP status. None of them is charged.
{
"error": {
"code": "insufficient_credits",
"message": "This request needs 5 credits; 3 are left this month."
}
}Match on error.code, not the message; messages may change. Some codes add fields next to code and message (below). Every error response also carries x-request-id, x-credits-used: 0 and x-credits-remaining; every 429 carries Retry-After.
All codes
| Status | Code | When | Charged | What to do |
|---|---|---|---|---|
| 400 | invalid_request | A parameter is missing, invalid or unknown (the message and error.issues name it). | No | Fix the request; retrying it unchanged fails again. |
| 401 | invalid_api_key | No key, an unknown key, or a revoked or expired key. | No | Check the Authorization header and the key on the API keys page. |
| 402 | insufficient_credits | Not enough credits left this month for this request, and billable usage is off. error.reason says why: free_credits_used, payment_failed or subscription_inactive. | No | Act on the reason at error.billing_url: add a card, update the payment method, or restart the subscription. Or wait for the 1st (UTC). |
| 402 | spend_limit_reached | Billable usage is on and this month’s spend limit is reached (error.reason: spend_limit). | No | Raise the spend limit at error.billing_url (the Billing page), or wait for the 1st (UTC). |
| 403 | forbidden_target | The URL is denylisted, resolves (or redirects) to a private or internal address, or sends cookies or credentials to a login, account or checkout page. | No | Do not retry. See the Acceptable Use Policy. |
| 403 | robots_disallowed | respect_robots is on (default for crawl and batch) and the site’s robots.txt disallows the URL for PageteerBot. | No | Skip the URL. Turn respect_robots off only for sites you own or may crawl. |
| 403 | org_suspended | The account is suspended. | No | Contact support. |
| 403 | feature_not_in_plan | Schema extraction on the free mode or while it is not available on this deployment. | No | Use selectors or structured, or add a card. |
| 403 | feature_not_enabled | The endpoint belongs to a feature that is off for your organization (enabled per organization by us). | No | Contact us if you need it. |
| 403 | proxy_not_configured | unblock "residential" without a team proxy saved in Settings → Proxy (404 from POST /v1/proxy/test). | No | Save your proxy URL in Settings → Proxy (see Bring your own proxy), or use another tier. |
| 404 | not_found | Unknown route, or a crawl or batch id that does not exist in your account. | No | Check the path and id. |
| 405 | method_not_allowed | The path exists, but not with this HTTP method (for example GET /v1/render). The Allow header lists the methods. | No | Use a method from the Allow header. |
| 409 | idempotency_conflict | An Idempotency-Key was reused with a different request body (POST /v1/batch or /v1/crawl). error.job_id names the original job. | No | Use a new key for a new job. |
| 409 | conflict | The resource is busy, e.g. canceling a job that is already running, or exporting a crawl that has not finished. | No | Wait for it to finish, then try again if still needed. |
| 413 | payload_too_large | The request body is over 64 KB (8 MB for POST /v1/batch), or a crawl is too large to export in one file. | No | Send less, or split a batch; read a large crawl with GET /v1/crawl/:id. |
| 422 | target_blocked | The site blocked every tier we tried (with unblock "auto", the tiers this deployment offers). block_reason, block_vendor and attempts say how. | No | Try a stronger unblock tier (or "auto"); if auto already failed, retry much later. |
| 422 | captcha_required | The site answered with a CAPTCHA or bot challenge. We never solve them and try no further tier. | No | Do not retry soon. Use the site’s API or data feed if it has one. |
| 422 | login_required | The site redirected to a login or sign-in page (final_url says where). We never log in. | No | The content is not public: skip it. |
| 422 | selector_not_found | A screenshot selector matched no visible element within 5 seconds of the page loading. Not charged. | No | Check the selector against the live page (it may differ by device or need a consent click), or capture the page instead. |
| 429 | rate_limited | Over your requests per minute, or too many requests to one site across the platform right now. | No | Wait Retry-After seconds, then back off with jitter. |
| 429 | concurrency_limit | Too many of your requests are in flight at once. | No | Wait for one to finish (Retry-After: 1) or lower your parallelism. |
| 429 | target_rate_limited | The site asked us to slow down (429, or 503 with Retry-After), or per-site pacing would not let your request start in time. We pause that site for everyone. | No | Wait error.retry_after seconds (also in Retry-After). |
| 504 | timeout | The site did not answer within timeout_ms. | No | Retry, maybe with a larger timeout_ms or mode "http". |
| 502 | proxy_failed | Your proxy failed: reason "auth" (it rejected the credentials), "unreachable", "status" (it refused the connection) or "blocked" (its host resolves to a private address). | No | Fix the URL or credentials in Settings → Proxy and test it. "unreachable" and "status" may pass on retry. |
| 500 / 502 / 503 | internal_error | Something failed on our side (rendering engine, LLM extraction or the API itself), or the rendering engine is at capacity (503, Retry-After: 5). | No | Retry with backoff (honor Retry-After); quote x-request-id if it persists. |
A POST body that is not a JSON object (for example sent as text/plain) gets 415 invalid_request; invalid JSON gets 400 invalid_request. Codes may be added over time; treat an unknown code by its HTTP status.
Why a request was refused (402)
A 402 (insufficient_credits or spend_limit_reached) always says why in error.reason and where to fix it in error.billing_url, your Billing page. credits_needed (what the request costs) and credits_remaining are added when known. Nothing is charged, and retrying does not help until the reason is fixed.
| reason | Code | Meaning | What to do |
|---|---|---|---|
free_credits_used | insufficient_credits | Free plan: this month’s free credits are used up. | Add a card (pay as you go) on the Billing page, or wait for the 1st (UTC). |
payment_failed | insufficient_credits | Paid plan, but the last invoice failed, so usage beyond the free credits is paused. | Update the payment method or pay the open invoice on the Billing page. |
subscription_inactive | insufficient_credits | Paid plan, but the subscription is not active (incomplete, canceled, paused…). | Restart the subscription on the Billing page. |
spend_limit | spend_limit_reached | This month’s spend limit is reached. | Raise the spend limit on the Billing page, or wait for the 1st (UTC). |
{
"error": {
"code": "insufficient_credits",
"message": "This request needs 5 credits; 3 are left this month.",
"reason": "free_credits_used",
"billing_url": "https://pageteer.vercel.app/app/billing",
"credits_needed": 5,
"credits_remaining": 3
}
}New reasons may be added: treat an unknown one like the code it comes with, and send the user to billing_url. Crawls and batches that stop for credits keep the insufficient_credits: … or spend_limit_reached: … prefix in their error and may carry the same reason.
Extra fields
| Name | Type | Description |
|---|---|---|
issues | { path, message }[] | invalid_request from validation: one entry per problem; path is the field, e.g. "formats.0". |
reason, billing_url, credits_needed, credits_remaining | string, string, integer, integer | insufficient_credits and spend_limit_reached: why it was refused and the Billing page that fixes it; the two counts only when known (see 402 reasons). |
retry_after, reason, target_status | integer, string, integer | target_rate_limited: seconds to wait (also in Retry-After); retry_after when the site pushed back or host_spacing when our per-site pacing would not fit your timeout; the site's status when it answered. |
block_reason, block_vendor, attempts | string, string | null, object[] | target_blocked: how the site blocked us and every tier tried (see below). |
provider, block_reason, status, attempts | string | null, … | captcha_required: the CAPTCHA or vendor, and the tries made. |
final_url | string | null | login_required: the login page we were sent to. |
reason, proxy_status | string, integer | null | proxy_failed: auth, unreachable, status or blocked (see Bring your own proxy), and the proxy's HTTP status when it answered. |
retryable | boolean | true on errors that are worth retrying as they are, such as a temporary DNS failure. |
job_id | string | idempotency_conflict: the batch the key already belongs to. |
{
"error": {
"code": "invalid_request",
"message": "Unknown field \"foo\" (see /openapi.json for the accepted fields)",
"issues": [{ "path": "", "message": "Unknown field \"foo\" (see /openapi.json for the accepted fields)" }]
}
}{
"error": {
"code": "target_rate_limited",
"message": "The target asked us to slow down, so we are pausing requests to it. Retry after 120 s. You were not charged.",
"retry_after": 120,
"reason": "retry_after",
"target_status": 503
}
}DNS errors
A domain that does not exist is 400 invalid_request ("Could not resolve host X: the domain has no DNS record. Check the URL."): fix the URL. A temporary DNS failure (resolver timeout, SERVFAIL) is 502 internal_error with retryable: true ("DNS lookup for X failed temporarily (…). Retry shortly. You were not charged."): retry it. Batches retry such URLs for you.
Retrying
- Retry
429afterRetry-Afterseconds, and500/502/504with exponential backoff and jitter (for example 1 s, 2 s, 4 s, at most 3 or 4 tries). See Rate limits for a loop. - Do not retry unchanged:
400,401,403,404,409,413and422, norproxy_failedwith reasonauthorblocked. They fail the same way again.402needs the reason fixed first. - Retrying a
POST /v1/crawlorPOST /v1/batchafter a5xxor a network error could start a second job, unless you send an Idempotency-Key: then retries are safe.
What is charged
Only successful responses are charged. When the target answers, that is a success even if its status is a 3xx or 4xx: a 200 from us with "status": 404 in the body is charged, because we did the work. A site that answers 5xx is not charged, and every error in the table above is free, including timeouts, blocked targets, CAPTCHAs and our own failures. See Credits.
target_blocked
The body carries block_reason, block_vendor (cloudflare, akamai, datadome, …, or generic; null when unknown) and attempts. block_reason tells you what to try next:
empty_response: the page had no visible text (often a JavaScript page). Retry withmode: "browser"if you usedhttp.status_403,status_429,challenge_page,block_page(the page says you were blocked): a strongerunblocktier may help; ifautoalready tried them all, give up or retry much later.
captcha_required
Pageteer does not solve CAPTCHAs. When a request meets one, on any tier, it stops and returns this error (a JavaScript check that clears by itself in a browser is first waited out once, as described in Unblocking), with provider set to the CAPTCHA (recaptcha_v2, hcaptcha, turnstile) or the bot-protection vendor (for example cloudflare) when we can tell, else null.
{
"error": {
"code": "captcha_required",
"message": "The target answered with a CAPTCHA or bot challenge (cloudflare). Pageteer does not solve CAPTCHAs, so we stopped without trying other tiers. You were not charged.",
"provider": "cloudflare",
"block_reason": "challenge_page",
"status": 403,
"attempts": [{ "tier": "none", "status": 403, "ms": 412, "blocked": true, "block_reason": "challenge_page", "captcha": true }]
}
}proxy_not_configured and proxy_failed
unblock: "residential" goes through your team's own proxy. Without one saved in Settings → Proxy you get 403 proxy_not_configured (and 404 from POST /v1/proxy/test). When the proxy itself fails you get 502 proxy_failed with a reason: auth (it rejected the credentials), unreachable, status (it refused to connect to the site) or blocked (its host resolves to a private address). Neither is charged. auth and blocked fail the same way until you fix the URL; the others may pass on retry. See Bring your own proxy.
{
"error": {
"code": "proxy_failed",
"message": "Your team proxy rejected its credentials (HTTP 407). Check the username and password in your proxy settings. You were not charged.",
"reason": "auth",
"proxy_status": 407,
"retryable": false
}
}Batch outcomes
In a batch, each URL that fails ends with one of these codes as its outcome (and in error.code), instead of an HTTP error. Batches also use ok for a fetched page, target_error when the site kept answering 5xx and canceled when the batch was canceled first. Crawl pages record the code in error. New codes may be added: treat unknown ones as a failure for that URL. Failed and canceled URLs are never charged.
The JavaScript client throws every error as a PageteerError with status, code, requestId, retryAfter, details and, on a 402, reason and billingUrl, and retries 429/5xx for you.