Expand AI logo
DocsAPI ReferenceAPI Reference
Glow Active
Login

API Reference

Getting Started

API ReferenceErrors

Endpoints

FetchFetch JSON ModeStart Batched FetchGet Batched StatusCancel Batched FetchGet CatalogSnapshot Highlights
Browse docs

Getting Started

API ReferenceErrors

Endpoints

FetchFetch JSON ModeStart Batched FetchGet Batched StatusCancel Batched FetchGet CatalogSnapshot Highlights

Errors

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 failureHow it is reported
API or captureNon-2xx status with a _tag-discriminated error body
Browser page origin200 plus a FetchOriginError in the body (or in response headers, on /v1/fetch)
File origin refusal404, 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_tagWhat happenedRetry
200FetchOriginError in the body or response headersExpand completed the request; the origin failedFollow 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.

{
  "_tag": "FetchOriginError",
  "url": "https://checkout.deadstartup.example/pricing",
  "kind": "dns",
  "upstreamError": "net::ERR_NAME_NOT_RESOLVED",
  "retryable": false
}
FieldDefinition
urlThe requested URL this outcome describes.
kindWhat went wrong at the origin. See the table below.
upstreamErrorThe 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

kindDefinitionretryable
dnsDNS resolution failed. The exact code distinguishes a stable missing name from resolver timeouts/failures.Depends on upstreamError.
tlsThe 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:

HTTP/1.1 200 OK

{
  "_tag": "FetchOriginError",
  "url": "https://checkout.deadstartup.example/pricing",
  "kind": "dns",
  "upstreamError": "net::ERR_NAME_NOT_RESOLVED",
  "retryable"

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.

HeaderValue
x-expand-origin-errorThe 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 OK
content-type: text/markdown; charset=utf-8
x-expand-origin-error: dns
x-expand-origin-error-detail: net::ERR_NAME_NOT_RESOLVED
x-expand-origin-error-retryable: false

# Origin unreachable

DNS resolution failed for `https://checkout.deadstartup.example/pricing`
(`net::ERR_NAME_NOT_RESOLVED`). No HTTP response exists at this URL. Retrying
will 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 OK
content-type: text/markdown; charset=utf-8
x-expand-origin-error: http
x-expand-origin-error-detail: 500
x-expand-origin-error-retryable: true

---
url: https://api-status.example/dashboard
title: 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.

{
  "status": "SUCCEEDED",
  "error": {
    "_tag": "FetchOriginError",
    "url": "https://checkout.deadstartup.example/pricing",
    "kind": "dns",
    "upstreamError


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:

{
  "_tag": "ServiceUnavailable",
  "retryable": true
}

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:

{
  "_tag": "HttpApiDecodeError",
  "message": "Invalid request parameters",
  "issues": [
    {
      "_tag": "Missing",
      "path": ["url"],
      "


Endpoint-level validation, such as unknown properties or a Highlights request with no content to highlight, produces BadRequest:

{
  "_tag": "BadRequest",
  "message": "Invalid fetch request body"
}

Common validation issues:

  • Missing required url field
  • Invalid URL format (must be http:// or https://)
  • Unknown top-level keys in the request body
  • 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"
}
ReasonDefinition
InvalidApiKeyThe API key is missing or invalid
InvalidTokenThe bearer token is invalid
InvalidSessionThe 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.

{
  "_tag": "AccessBlocked",
  "organizationId": "org_123"
}

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.

{
  "_tag": "FetchBlocked",
  "url": "https://example.com",
  "blockedType": "botProtection"
}

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.

{
  "_tag": "FetchPageTooLarge",
  "url": "https://example.com/huge",
  "limitKind": "domsnapshot_bytes",
  "limitBytes": 52428800,
  "measuredBytes": 91234567
}

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:

{
  "_tag": "FetchConcurrencyLimitExceeded",
  "maxConcurrency": 5,
  "retryAfterSeconds": 20,
  "requestId": "0f4c...",
  "retryable": true
}

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.

{
  "_tag": "FetchNavigationFailed",
  "url": "https://example.com/report.pdf",
  "failureType": "browserInternalDocument",
  "retryable": false
}

FetchCaptureFailed means navigation succeeded, but the capture did not:

{
  "_tag": "FetchCaptureFailed",
  "url": "https://example.com/",
  "failureType": "pageNeverSettled",
  "retryable": false
}
failureTypeDefinition
pageNeverSettledThe page never produced any navigation activity to capture
capabilityFailedThe 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.

{
  "_tag": "FetchUnsupportedContentType",
  "url": "https://example.com/archive.7z",
  "contentType": "application/x-7z-compressed"
}

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.

{
  "_tag": "FetchOriginError",
  "url": "https://example.com/report.pdf",
  "kind": "http",
  "originStatusCode": 404,
  "contentAvailable": false,
  "retryable

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".

{
  "_tag": "FetchDocumentProcessingUnavailable",
  "url": "https://example.com/scan.pdf",
  "operation": "ocr",
  "message": "OCR provider unavailable"
}

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.

{
  "_tag": "FetchPageTooLarge",
  "url": "https://example.com/book.pdf",
  "limitKind": "document_pages",
  "limit": 50,
  "measured": 312
}

Capture timeouts (504)

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:

{
  "_tag": "FetchCaptureTimeout",
  "url": "https://example.com/",
  "timeoutMs": 150000,
  "retryable": false
}
  • 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:

{
  "_tag": "FetchCapacityTimeout",
  "url": "https://example.com/",
  "timeoutMs": 150000,
  "retryable": true
}
  • 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.

On This Page

How errors are reportedError taxonomyError response formatOrigin outcomes (200)KindsPOST /v1/fetch/jsonPOST /v1/fetchBatched FetchNon-2xx errorsretryableError typesValidation errors (400)Authentication errors (401)Access and blocked errors (403)Size errors (413)Rate limit errors (429)Capacity errors (429)Internal errors (500)Page capture errors (502)Document and video errors (404, 410, 413, 415, 422, 502, 503)Capture timeouts (504)Capacity timeouts (529)Service errors (503)
502
FetchOriginHttpError
HttpApiDecodeError
BadRequest
The request body did not match the schema
Never
401AuthFailedThe API key, token, or session is not validNever
403AccessBlockedYour organization's access is suspendedNever
403FetchBlockedThe target site refused the capture (bot protection, captcha)Never
404FetchSnapshotHighlightsNotFoundNo Snapshot exists to searchNever
404, 410FetchOriginHttpErrorThe origin reports the requested document is missing or goneNever
409BatchedIdempotencyConflict, BatchedCancellationConflictThe batched run is in a conflicting stateNever
413PayloadTooLargeThe request body exceeds the limitNever
413FetchPageTooLargeThe captured page or document conversion exceeds a limitNever
415FetchUnsupportedContentTypeThe origin served content no converter handlesNever
422FetchVideoUnavailableThe video platform will not serve this video; inspect availabilityOnly for live and upcoming, after the stream ends
429TooManyRequestsYou exceeded your rate limitretryable: true
429FetchConcurrencyLimitExceededYou already have as many captures running as your browser concurrency allowsretryable: true
500InternalErrorAn unexpected server-side failure interrupted the requestretryable: true
502FetchNavigationFailedThe browser could not navigate to the pageretryable: false
502FetchCaptureFailedThe page could not be capturedretryable: false
502FetchOriginHttpErrorThe origin refused a document request; inspect statusCodeRetry later
503FetchDocumentProcessingUnavailableOCR, transcription, or video acquisition failed after provider retriesRetry later
503ServiceUnavailableA transient failure on our sideretryable: true
504FetchCaptureTimeoutThe capture started but produced no result before its deadlineretryable: false
529FetchCapacityTimeoutThe request expired in the queue; the capture never startedretryable: true
The origin's HTTP status. Present only for kind: "http".
retryableWhether retrying the same URL later could plausibly succeed.
contentAvailablefalse for a standalone HTTP-origin refusal. No document was delivered and the request is refunded.
redirectLimitReachedtrue when the origin exceeded the document redirect limit.
finalUrlThe last URL that responded, when recorded for a document refusal.
connectionRefusedThe connection attempt was explicitly refused; nothing accepted it.false
connectionResetA peer or intermediary reset an established connection (TCP RST).true
connectionClosedA peer closed an established connection unexpectedly (TCP FIN).true
addressUnreachableThe network had no working route to the origin address.true
emptyResponseThe server accepted the connection, then closed it without sending data.true
connectionTimeoutThe connection never answered in time.true
httpThe origin responded, with a status of 400 or above.Depends on the status. See below.
:
false
}
"json": [],
"originError": {
"_tag": "FetchOriginError",
"url": "https://site.example/removed-page",
"kind": "http",
"originStatusCode": 404,
"retryable": false
}
}
The net::ERR_* code for the network kinds, or the origin's status for http. Omitted when neither is known.
x-expand-origin-error-retryabletrue or false.
x-expand-content-availablefalse for the upcoming document-refusal outcome. The body explains the refusal and contains no captured document.
"
:
"net::ERR_NAME_NOT_RESOLVED"
,
"retryable": false
}
}
message
"
:
"is missing"
}
]
}
The organization was not found
"
:
false
}