# Schema-driven structured extraction

Fetches `url` and extracts structured JSON conforming to the supplied
JSON Schema (draft 2020-12) using an LLM. Fast-path only — slow-path
rendering options (`wait_until=networkidle`, `delay_ms`,
`wait_for_selector`) cause a `400 unsupported_option`.

Dual-axis billing: a fixed per-request credit charge **plus** the
underlying page's bandwidth, billed at its upstream wire bytes with no
endpoint factor (a 100 KiB estimate is pre-charged, then reconciled). The
bandwidth is still billed when the scrape succeeds but extraction later
fails; both axes are refunded only when the scrape itself never runs.

**POST** `/v1/schema`

## Request Body

- `url` (required)
- `schema` (required)
- `prompt`
- `model`
- `wait_for_selector`
- `wait_until`
- `delay_ms`
- `country`
- `language`

## Responses

### 200 — Extraction completed (success or graceful extract failure).
- `data`
- `model`
- `validated`
- `metrics`
- `metrics.scrape_ms`
- `metrics.llm_total_ms`
- `metrics.input_tokens`
- `metrics.output_tokens`
- `metrics.retries`
- `metrics.total_ms`
- `scrape`
- `scrape.url`
- `scrape.status_code`
- `scrape.content_length`
- `scrape.truncated`
- `warnings`

### 400 — Invalid URL (`invalid_url`), unsupported model (`unsupported_model`), or unsupported slow-path option (`unsupported_option`).
- `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 (`validation_error`), non-normalizable schema (`schema_incompatible`),
or the fetched page's HTML could not be converted for extraction — over the size cap or
past the conversion deadline (`content_too_large`). On `content_too_large` the page's
bandwidth is billed and the credits are refunded.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 429 — Per-account rate limit (`rate_limited`) or upstream provider rate limit (`provider_rate_limited`); a `Retry-After` header may be present.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 500 — Internal error (`internal_error`): the request could not be recorded, or its
bandwidth charge could not be reserved. The credits held for it are refunded.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 503 — Read `error.code` to tell the conditions apart: underlying scrape failed or the
extraction service was unreachable (`scrape_failed`), upstream provider error
(`provider_error`), extraction provider unconfigured (`provider_unavailable`), or
upstream provider timed out (`provider_timeout`). None of these answers `502` or
`504`: Cloudflare fronts this API and replaces an origin 502 or 504 with its own
`text/plain` page, discarding the error envelope, while a 503 passes through
unchanged (#4891).
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`