# Search the web

Runs a web search for `query` and returns ranked organic results with
contiguous `position` values from 1.

Use `limit` (1–100, default 10) to request more results in a single
call. Results are fetched in pages of 10, so a call consumes
`ceil(limit / 10)` credits (capped at 10) from the account's monthly
credits bucket — for example `limit=10` costs 1 credit and `limit=35`
costs 4. Credits are charged for every page fetched regardless of how
many results a page returns. The credit count is reported as
`credits_used` in the response.

**POST** `/v1/search`

## Request Body

- `query` (required)
- `type`
- `limit`
- `country`
- `language`
- `date_range`

## Responses

### 200 — Search completed.
- `id`
- `query`
- `results`
- `results.position`
- `results.title`
- `results.url`
- `results.snippet`
- `results.date`
- `results.source`
- `results.domain`
- `results.image`
- `results.image.url`
- `results.image.width`
- `results.image.height`
- `results.image.thumbnail_url`
- `results.image.thumbnail_width`
- `results.image.thumbnail_height`
- `related_searches`
- `credits_used`

### 400 — Bad request (e.g. missing or empty `query`).
- `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`

### 429 — Rate limit exceeded (`rate_limited`). Carries the `X-RateLimit-Limit`/`X-RateLimit-Remaining`/`X-RateLimit-Reset` triplet and a `Retry-After` header. The window is per-second for a bearer API key, or the 60-request/60-second Clerk session window when authenticated via `resolveDualAuth` — `Retry-After` reflects whichever window applies to the request.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 500 — Internal error (code `internal_error`) — the search request could not
be recorded before the provider call. The pre-incremented credits are
refunded and no provider call is made.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 503 — Two different bodies travel under this status. Tell them apart by the
type of the top-level `error` key: a string is the first, an object
is the second.

- **Upstream search provider error**, answered at `503` rather than
  `502` because Cloudflare replaces an origin 502 body with its own
  page (#4891). The body is a fixed, vendor-masked shape —
  deliberately NOT the unified `Error` envelope. The upstream
  provider's identity and message are never surfaced, so the
  response carries only the constant `error` code
  `search_provider_error`.
- **Billing entitlement unavailable** (`billing_entitlement_missing`),
  raised before any search runs, when the account has a Stripe
  subscription but its plan could not be resolved. This one is the
  unified `Error` envelope. No search is run and nothing is charged.
- `error`
- `status`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`