Every error Expand returns, with its status, payload, and retry verdict.
Expand reports origin failures separately from API and capture errors. Check the status and the _tag together: browser page Fetches and file Fetches report origin refusals differently.
This page lists every error and its payload. How to handle them, which to retry, and what you are charged for is in the Errors & retries guide.
How errors are reported
A 200 can still report an origin failure. A non-2xx can be an API or capture error, or an origin refusing to serve a file.
Fetching a URL involves two parties. Expand can fail: infrastructure trouble, a bot wall it could not get past, a page beyond its processing limits. The origin can also fail: DNS that does not resolve, a broken certificate, a refused connection, a 404. Browser page origin outcomes are typed data inside a 200 response. A file origin's refusal can return 404, 410, or with .
502
FetchOriginHttpError
Whose failure
How it is reported
API or capture
Non-2xx status with a _tag-discriminated error body
Browser page origin
200 plus a FetchOriginError in the body (or in response headers, on /v1/fetch)
File origin refusal
404, 410, or with in the body
The practical consequence: a permanently missing page or file is not mistaken for a temporary outage. A browser page's origin outcome returns 200; a missing file returns 404 or 410. Read the tag and retry verdict before retrying a 502, which can also be a file-origin refusal.
Error taxonomy
Every error carries a _tag naming its class, and the classes whose status alone cannot settle the
question also carry a retryable verdict. Status and tag together are the contract.
The tag is what you should branch on.
Status
_tag
What happened
Retry
200
FetchOriginError in the body or response headers
Expand completed the request; the origin failed
Follow the origin outcome's retryable field
400
,
A successful request returns 200. Statuses that carry more than one tag return them as an anyOf
in the OpenAPI spec: the status tells you the category, the _tag tells you which member of it.
For browser navigation and capture failures, a proven origin failure returns 200 with a
FetchOriginError. A deterministic page or capture failure returns 502 with retryable: false.
An infrastructure failure can return 503 with retryable: true. A missing or unreviewed
network code is not enough to blame the origin or the page.
Every response, success or error, carries the x-expand-trace-id header. Quote it when contacting support.
Error response format
Every error body carries a _tag field identifying the error type:
{ "_tag": "ErrorType" // additional fields depend on the error type}
Origin outcomes (200)
For a browser page, an origin failure returns 200 carrying a FetchOriginError. A file origin's refusal can instead return non-2xx FetchOriginHttpError as described below.
What went wrong at the origin. See the table below.
upstreamError
The underlying browser network error, for the network kinds. Optional.
originStatusCode
The document-only fields contentAvailable, redirectLimitReached, and finalUrl describe the upcoming document-origin outcome. Document refusals return FetchOriginHttpError with HTTP 404 or 410 when the document is missing or gone, and 502 for other origin statuses.
Kinds
kind
Definition
retryable
dns
DNS resolution failed. The exact code distinguishes a stable missing name from resolver timeouts/failures.
Depends on upstreamError.
tls
The TLS handshake or certificate validation failed.
Depends on upstreamError.
The network kinds mean no HTTP response ever existed. http means the origin answered and told you it had a problem.
For current captures, retryable is derived from the exact reviewed Chromium net::ERR_* code, not merely from kind. For example, net::ERR_NAME_NOT_RESOLVED is a stable DNS failure and reports false, while net::ERR_DNS_TIMED_OUT is transient and reports true. Unknown codes from current native capture stay Expand-side failures rather than receiving a guessed origin kind or retry verdict. Failures stored before Expand recorded the network code may not carry one. When such a record has a failureKind from that earlier format, the API keeps that kind and uses its conservative default retry verdict.
For kind: "http", retryable is true when originStatusCode is 408, 429, or 500 and above. The origin either asked for a later attempt or hit a server-side condition that may clear. Every other 4xx describes the request itself, so repeating it unchanged reports false.
POST /v1/fetch/json
The JSON endpoint returns one of two shapes, and they are disjoint.
Origin never reached. No document exists, so the origin outcome is the response. Recognize this variant by its top-level _tag:
Origin responded with an error status. The page was captured, so you get the content and the outcome. Error pages are frequently useful: soft-404s, maintenance notices, status dashboards returning 500. Recognize the content variant by its meta field. It has no top-level _tag.
HTTP/1.1 200 OK{ "meta": { "version": 1, "url": "https://site.example/removed-page", "title": "Page not found" }, "markdown": "# Page not found\n\nThe page you are looking for has been removedโฆ",
originError is absent whenever the origin answered normally, so its presence alone tells you the origin reported a problem.
POST /v1/fetch
/v1/fetch answers in text/markdown, which has nowhere to put a typed field, so the origin outcome rides response headers.
Header
Value
x-expand-origin-error
The kind: dns, tls, connectionRefused, connectionReset, connectionClosed, addressUnreachable, emptyResponse, connectionTimeout, or http.
x-expand-origin-error-detail
When the origin was never reached there is no document to return, so the body is a short Markdown error document written to be read by an LLM as readily as by a human:
HTTP/1.1 200 OKcontent-type: text/markdown; charset=utf-8x-expand-origin-error: dnsx-expand-origin-error-detail: net::ERR_NAME_NOT_RESOLVEDx-expand-origin-error-retryable: false# Origin unreachableDNS resolution failed for `https://checkout.deadstartup.example/pricing`(`net::ERR_NAME_NOT_RESOLVED`). No HTTP response exists at this URL. Retryingwill not succeed until the domain's DNS is fixed.
When an error page was captured, its content is returned as usual and only the headers change:
HTTP/1.1 200 OKcontent-type: text/markdown; charset=utf-8x-expand-origin-error: httpx-expand-origin-error-detail: 500x-expand-origin-error-retryable: true---url: https://api-status.example/dashboardtitle: Internal Server Error---# Internal Server Errorโฆcaptured page contentโฆ
The headers are absent entirely when the origin answered normally.
The SDK's fetch() returns the Markdown string and drops the headers. Use fetchJson(), which carries the same facts in originError, or a direct HTTP request when you need the headers themselves.
Batched Fetch
Batched Fetch reports the same type per URL, in-band. A run never fails because one URL was unreachable.
An item carries exactly one of data or error: error means no Snapshot was produced. An origin that responded with an error status did produce one, so that item keeps its data and reports the outcome in a separate originError field alongside it. See Batched Fetch: error and originError.
Non-2xx errors
retryable
A status code cannot always tell you whether a retry could succeed: a 502 can be a
deterministic page failure or a file-origin refusal, while a 503 is a transient service
failure. Most API errors whose status leaves that open (429, 500, 502, 503) state
the answer outright:
Prefer this field over the status code. FetchOriginHttpError is an exception: its 502
body has no retryable field, and the SDK retries it according to the status. Errors
that are unambiguous from their status alone (400,
401, 403, 404, 409, 410, 413) omit it. Treat a missing field on those as false.
429 and 503 responses also carry a Retry-After header, in seconds. Wait at least that long
before retrying. Retrying sooner spends your own timeout budget on a refusal we already predicted.
Error types
Validation errors (400)
Returned when the request body does not match the expected schema. Framework-level decoding produces HttpApiDecodeError:
Invalid regex pattern in include.links.includePatterns
Empty highlights.query
Authentication errors (401)
{ "_tag": "AuthFailed", "reason": "InvalidApiKey", "description": "The provided API key is not valid"}
Reason
Definition
InvalidApiKey
The API key is missing or invalid
InvalidToken
The bearer token is invalid
InvalidSession
The session has expired
InvalidTenant
Access and blocked errors (403)
AccessBlocked means your organization cannot currently run fetches, most often because there are too few credits to cover the request. See Pricing & Usage.
FetchBlocked means the target site blocked the capture and Expand could not get past it. This is Expand failing to deliver the page, which is why it is a 403 rather than an origin outcome.
blockedType is optional and describes the classification, for example captcha, botProtection, ipBlock, or authWall. Retrying immediately rarely helps. Expand already retried through a different proxy exit where that could plausibly clear the block. See Browser-rendered pages: Blocked pages.
Size errors (413)
PayloadTooLarge is about your request. FetchPageTooLarge is about the target page: the request was well-formed, the page is simply beyond what Expand will process.
limitKind is one of transfer_bytes, domsnapshot_bytes, or document_frame_bytes. Older stored results can also carry ast_bytes, a retired limit that is no longer produced. Retrying the same URL will not help.
Rate limit errors (429)
Returned when you exceed your rate limit. See Limits.
{ "_tag": "TooManyRequests", "retryable": true}
Capacity errors (429)
Returned when your organization already has as many captures in flight as your browser concurrency allows. Nothing
was captured, so the same request succeeds once a slot frees:
maxConcurrency is your own limit, not a global one. Either lower the number of requests you run at
once, or send the URLs to /v1/fetch/batched, which queues against the same limit instead of
refusing. See Limits.
Internal errors (500)
Returned when an unexpected server-side failure interrupts the request:
{ "_tag": "InternalError", "retryable": true}
An unexpected failure may not recur, so a retry is worthwhile.
Page capture errors (502)
Returned when the browser could not produce a usable page for the URL. Two tags share this status.
FetchNavigationFailed means navigation itself did not reach the page. Navigation committed no HTTP(S) document, and no recognized Chromium network code proved the origin was responsible. Browser documents such as about:blank, blob:, data:, extension or PDF viewers, and download outcomes are not evidence of a refused origin connection, so they remain Expand-side failures. Batched Fetch results stored before Expand recorded the network code, which carry only navigationFailed, return this error too.
The page never produced any navigation activity to capture
capabilityFailed
The page rendered, but no capture satisfied a format you requested
Both verdicts are deterministic for this URL: the captured evidence proves another attempt will
reach the same outcome. Fix the URL or the target page instead.
Document and video errors (404, 410, 413, 415, 422, 502, 503)
Returned when the URL served a document rather than a page and the conversion could not be delivered. PDFs and other files lists what Fetch converts, and its Document errors section explains each one.
415 FetchUnsupportedContentType means nothing here can be converted. That covers an unknown format, a file too damaged to read, and a request that bounced between the page and document paths without either one handling it. contentType is present when Fetch could detect it.
An unavailable document is not charged. Document refusals return FetchOriginHttpError with the origin status in statusCode. Missing or removed documents return HTTP 404 or 410. Check the URL and access permissions before trying again. Other origin statuses return HTTP 502.
The upcoming response below uses 200 FetchOriginError with contentAvailable: false. Clients should support both shapes during the transition. originStatusCode is what the origin sent. Redirect exhaustion also sets redirectLimitReached: true and includes the last responding finalUrl. It does not necessarily mean a loop. The redirect chain may simply be too long.
This is deliberately unlike a page fetch, which renders an origin's error page. Rendering the error page is right for a caller who asked for a page. There is no rendering step for a document, so the only alternative would be converting a block page into Markdown and calling it the document.
503 FetchDocumentProcessingUnavailable means a vision or transcription provider could not finish the work, or, for a YouTube URL, that the platform refused to hand over the video. operation says which one: "ocr", "transcription", or "video".
422 FetchVideoUnavailable means the video platform will not serve this video to anyone in our position, so there is no transcript to return. availability is one of private, removed, geoRestricted, ageRestricted, membersOnly, live, upcoming, or unknown. Only live and upcoming change on their own, once the stream ends. YouTube answers 200 for all of them, which is why this is not a 502 FetchOriginHttpError. See YouTube videos: Video errors.
{ "_tag": "FetchVideoUnavailable", "url": "https://www.youtube.com/watch?v=aBcDeFgHiJk", "availability": "private", "message": "Private video. Sign in if you've been granted access to this video"}
413 FetchPageTooLarge means the document exceeded a limit. This can be an explicit documentConfig.maxPages limit, maxAudioSeconds, or a limit on image or audio processing. PDFs have no default page limit or public 50 MB transfer cap. Worker resource failures are reported separately. Readable pages use their text layer. Pages that need OCR are processed separately. limitKind tells you which one. For document_pages and audio_seconds the payload reports limit and measured in that unit, while the byte-shaped limits use limitBytes and measuredBytes. Nothing converts, and you are charged no credits.
Returned when processing started but no result was ready inside its budget: 150 seconds for browser captures or 55 minutes for documents. It is also
the conservative fallback when Expand cannot tell whether the capture started:
Browser Fetch has one 150-second budget per request that starts when the request is
accepted. Queueing, retries, rendering, and delivering the result all count against it. When it
expires, Expand takes up to five more seconds to decide the outcome. A result that lands in that
window is still returned. Otherwise you get this typed timeout by 155 seconds at the latest.
timeoutMs reports the configured request ceiling: 150000 for browser captures or 3300000 for documents, not the time remaining.
retryable: false is why the TypeScript SDK does not retry this automatically: it keeps ambiguous 504s out of
automatic retry loops, so an Expand-side incident is not amplified. It is not proof the page was
at fault, and a deliberate manual retry may still be appropriate. It starts a separate capture
that can consume another full 150-second budget.
Use a client timeout of 180000 ms for browser captures or 3600000 ms for documents. For browser captures, the server produces a typed timeout by 155
seconds at the latest, leaving 25 seconds for the response to reach you. The TypeScript SDK's timeoutMs default of 60000 aborts long captures client-side. See
TypeScript SDK.
Each item in a batch gets its own budget when its capture is scheduled. An item that never
started before its budget expired reports FetchCapacityTimeout. One that started stays on the
504 side.
Capacity timeouts (529)
Returned when the request was still waiting for capacity when its deadline passed:
This is Expand capacity exhaustion, not a verdict about the target page: nothing ran, so the same
request can succeed once a slot frees. That is what retryable: true says, and it is the one
timeout the TypeScript SDK retries for you.
Responses carry Retry-After: 5, a short overload backoff that avoids an immediate retry
stampede while spending little of the next request's 150-second budget. Direct HTTP clients should
wait at least that long.
If the capture started and then failed to finish, the request stays 504 FetchCaptureTimeout.
Service errors (503)
ServiceUnavailable means an Expand-side infrastructure problem. An unreachable origin is reported as 200 with a FetchOriginError instead.
{ "_tag": "ServiceUnavailable", "message": "Expand could not complete the request. Retry shortly, or contact support with the request ID if the problem persists.", "requestId": "0f4c...", "retryable": true}
This is reserved for transient failures on our side. Retry after the delay named by Retry-After,
and quote requestId if the problem persists. requestId is the same trace ID sent in the
x-expand-trace-id header.
502
FetchOriginHttpError
HttpApiDecodeError
BadRequest
The request body did not match the schema
Never
401
AuthFailed
The API key, token, or session is not valid
Never
403
AccessBlocked
Your organization's access is suspended
Never
403
FetchBlocked
The target site refused the capture (bot protection, captcha)
Never
404
FetchSnapshotHighlightsNotFound
No Snapshot exists to search
Never
404, 410
FetchOriginHttpError
The origin reports the requested document is missing or gone