# Fetch a URL (POST)

Same as GET /v1/fetch but with a JSON request body. The response is the same, including
the `X-Fetch-Fast-Path` rule for what the body holds: the upstream bytes verbatim when
it is `true`, the rendered page when it is `false`.

**Limits:** request bodies are capped at 512 KB. An oversized body
returns a `400` with error code `request_too_large`.

**POST** `/v1/fetch`

## Request Body

- `url` (required)
- `country`
- `timeout_ms`
- `cache_ttl`
- `custom`

## Responses

### 200 — Content from upstream.

### 400 — Invalid URL (`invalid_url`) or oversized request body.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 401 — Unauthorized. The request carried no valid credential, and no work was
performed. The unified `Error` envelope carries one of the codes below
in `error.code`; match on the code, never on the prose.

- `invalid_api_key` — a bearer API key was presented and rejected:
  unknown, revoked, expired or malformed. The bearer-only operations
  (`/v1/webhook-deliveries*` among them) also return it when no
  `Authorization` header is sent. `GET /v1/account` is the exception
  to "malformed": it returns `invalid_api_key` only for a key that
  starts `sk_test_` or `sk_live_` and is unknown, revoked or expired.
- `unauthorized` — the `Authorization` header is absent or malformed on
  an operation that also accepts a dashboard session (the dual-auth and
  billing operations). `GET /v1/account` also returns it, for no
  `Authorization` header and for any bearer value that does not start
  `sk_test_` or `sk_live_` (that value is checked as a dashboard
  session token).

Not retryable as-is: supply a valid API key first.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 402 — Payment required. The account cannot fund this request, and no work was
performed. The unified `Error` envelope carries one of the billing codes
below in `error.code`; match on the code, never on the prose.

- `quota_exceeded` — the enforced balance for this request's meter
  cannot fund it. Capture endpoints (`/v1/scrape`, `/v1/fetch`,
  `/v1/screenshots`, `/v1/sitemap` and the bulk siblings) meter
  bandwidth: they reject when the balance cannot fund the request's
  byte reservation, and `error.details.required_bytes` and
  `error.details.available_bytes` report the bytes the request reserves
  and the bytes available. `/v1/search` meters credits.
- `spending_cap_reached` — the request would push overage spend past the
  account's configured cap. Only reachable on a custom overage-enabled
  plan; standard plans stop at `quota_exceeded`.
- `billing_entitlement_impaired` — the paid subscription is in a payment
  impairment (e.g. `past_due`). `error.message` names the reason.
- `billing_entitlement_expired` — the paid subscription entitlement
  period has ended, or its dunning grace period has run out.

Not retryable as-is: settle the balance, raise the cap, or restore the
subscription first. This response carries no `X-Quota-*` headers — they
travel with the 2xx responses of the operations that emit them.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 422 — Validation error (e.g. malformed JSON, unsupported fetch format, or slow-path-only
field), or a fetch that failed for a target-side reason.

Two distinct conditions answer `422`. Read `error.code` to tell them apart.

- **`validation_error`** — the request itself was rejected.
- **`blocked`** or **`upstream_error`** — the fetch ran and the target could not
  be read. `blocked` means the target's bot protection refused every attempt;
  `upstream_error` (message `Failed to fetch URL.`) is any other exhausted-retries
  failure, such as a host that does not resolve. This is a result about the URL,
  not an outage of this API, so it is a `4xx` rather than `502` or `503`. No
  upstream HTTP status was received, so `error.http_status` reports `422`,
  mirroring what was served — unlike the relayed `upstream_error` under `503`
  below, whose `error.http_status` is the target's own. A target `502` or `504`
  ends here too whenever it does not meet the condition for `503` below. The
  request transfers no billable bytes and is charged nothing (#4669).
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 429 — Rate limit (`rate_limited`) or concurrency limit (`concurrency_limit`) exceeded. Carries a `Retry-After` header — seconds derived from the same clock as `X-RateLimit-Reset` for the rate-limit variant, a fixed small back-off for the concurrency variant.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 503 — Two distinct conditions answer `503`, and they disagree about what the body's
`error.http_status` reports. Read `error.code` to tell them apart.

- **`upstream_error`** — the target returned HTTP `502` or `504`, relayed here as
  `503`. This happens only when the escalation ladder ended on that answer, or when a
  cached copy of it is served. A `502` or `504` is not terminal, so ByteKit first
  retries it through its proxy tiers and the browser fallback; if the ladder ends any
  other way, or a bot-protection vendor was seen on the way, the request answers
  `422` (`upstream_error` or `blocked`) instead.
  Cloudflare fronts this API and replaces a clean origin 502 or 504 with its
  own generic `text/plain` error page, discarding the envelope before it reaches
  you, while a 503 is passed through unchanged. The body's `error.http_status`
  still reports the **true upstream status**, `502` or `504` — not `503` — and is
  the only surviving record of what the target answered, so the body rather than
  the HTTP status is authoritative here (#3355, #4891). Every other non-2xx
  upstream response is relayed at its own HTTP status with the same
  `upstream_error` envelope, so any other 4xx/5xx the target returns surfaces as an
  Error envelope rather than a raw body (#1462).
- **`internal_error`** (message `Fetch service unavailable.`) — no fetch service
  endpoint could be resolved on this deployment, a fault of this API and not of the
  target. There is no upstream status to preserve: the `503` is this API's own
  choice of wire status, so the body's `error.http_status` reports `503`, mirroring
  what was served (#3373). A fetch that ran and failed for a target-side reason
  answers `422` instead (#4669).
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`