Every error the API returns shares one flat shape, so you can handle them with a single code path. This page covers that shape, the full list of codes, and how to back off cleanly when you hit a limit.

The error model

Every error response — on every endpoint — has the same envelope:
Error envelope
code
string
A stable, machine-readable identifier. Branch on this — never on error.
error
string
A human-readable, actionable explanation. Validation errors name the offending field and the allowed values. Safe to log; may change over time.
request_id
string
The correlation id for this request. Always matches the X-Request-Id response header. Include it in support requests. You can also send your own X-Request-Id header (8–128 chars of [A-Za-z0-9._-]) and the API will echo it back.
docs_url
string
A deep link to the failing code’s section of this page.
details
array
Optional — present on some VALIDATION_FAILED responses with per-field issues.
The HTTP status carries the status; it is not duplicated in the body. Every response — success or error — also carries X-Request-Id and X-AISearch-Version headers.

Error codes

AUTH_INVALID

401 — The API key is missing, malformed, revoked, or unknown. On the core data-plane endpoints (POST /v1/search, GET /v1/jobs, /v1/usage, /v1/artifacts), a missing Authorization header and an invalid key are indistinguishable — both return this. Add Authorization: Bearer <API_KEY>.

AUTH_MISSING

401 — No Authorization header was sent. Emitted only by the account-management endpoints; the core data-plane endpoints return AUTH_INVALID for a missing key instead.

SCOPE_FORBIDDEN

403 — The API key is not scoped for the requested method. The message names the methods the key allows.

INSUFFICIENT_CREDITS

402 — Not enough credits for the request (or, on batch, for the whole batch — the reservation is atomic, so nothing is spawned).

VALIDATION_FAILED

400 — A request field is invalid. The message names the offending field and the allowed values (e.g. a malformed regions[i].country). Unknown top-level fields are rejected, not ignored — the API fails loud rather than silently dropping something that looks like a control, and the message names the unexpected field(s). Send only the documented fields (query/prompt, surfaces, regions/region/country, include, webhook, idempotencyKey). This also fires for unknown include keys — the only accepted keys are include.markdown and include.html; anything else is rejected here rather than silently dropped.

UNSUPPORTED_SURFACE

400 — A value in surfaces isn’t a supported surface. The message lists every valid surface. See surfaces.

UNSUPPORTED_METHOD_FOR_SURFACE

422 — The surface has no live v1 capture path yet. The message names what IS supported. GET /v1/surfaces is the live capability matrix.

REGION_UNAVAILABLE

422Reserved; not currently emitted. The canonical submit path accepts any valid ISO-3166 country and validates country for format only — it never gates a region per surface, so this code does not fire today. A malformed country is a VALIDATION_FAILED (400) instead. The code is held in reserve for a future live per-surface region gate; don’t write handling that depends on it firing now.

TOO_MANY_REGIONS

422 — More than 10 regions in one request. Each region multiplies billed children; split the request instead.

QUERY_TOO_LONG

422 — The query (or its prompt alias) is over 2000 characters. Shorten the prompt, or split the work across several requests.

IDEMPOTENCY_CONFLICT

409 — The same idempotencyKey was reused with a different body. Use a new key or resend the original body.

JOB_NOT_FOUND

404 — The job id doesn’t exist or isn’t yours (reads are owner-scoped; another tenant’s id is a 404, never a 403).

WATCH_NOT_FOUND

404 — No watch (or watch run) exists with the given id, or it belongs to another tenant (reads and writes are owner-scoped — another tenant’s id is a 404, never a 403).

WATCH_LIMIT_EXCEEDED

403 — Creating this watch would exceed the plan’s active-watch ceiling. The message names the ceiling and the plan. Pause or cancel an existing watch, or upgrade. See Watches.

WATCH_INTERVAL_TOO_SHORT

422 — The requested schedule is below the plan’s minimum interval. The message names the floor and the plan. See Watches.

ARTIFACT_NOT_FOUND

404 — The artifact key doesn’t exist or isn’t yours.

NOT_FOUND

404 — The requested path doesn’t exist on this API. Check the URL against the API reference — every data-plane route lives under /v1/.

METHOD_NOT_ALLOWED

405 — Wrong HTTP method for the endpoint (e.g. POST /v1/health).

RATE_LIMIT_EXCEEDED

429 — You’ve exceeded your request rate. Honor Retry-After and back off.

CONCURRENCY_LIMIT_EXCEEDED

429 — Too many in-flight sync requests at once. Retry after a short delay, reduce parallelism, or use ?mode=async.

QUEUE_CAPACITY_EXCEEDED

429 — The async job queue is temporarily saturated. Back off with jitter and retry.

REQUEST_CANCELED

499 — The request was canceled before completion.

ACQUISITION_FAILED

502 — The capture failed upstream after admission.

EMPTY_ANSWER

502 — An AI-assistant surface (chatgpt, claude, perplexity, gemini, copilot) reported a rendered answer but produced no extractable text. We refuse to bill an empty completion, so the capture is failed and the credits are refunded. Retry, or submit async and poll the job. (Google Search/News and AI Overview/Mode surfaces are unaffected — an empty result there is genuine absence, returned as completed with a surface_absent warning.)

UPSTREAM_BAD_GATEWAY

502 — An upstream provider returned an unusable response.

DRIVER_UNAVAILABLE

503 — The capture lane for the requested surface is unavailable (we fail loud rather than serve a mock as real). Retry with backoff, or submit with ?mode=async and poll the job.

SUBMISSION_FAILED

503 — The request passed validation but couldn’t be durably enqueued. No job was created (the credit reservation is released) — retry with backoff.

WEBHOOK_SECRET_UNAVAILABLE

503 — Your account’s managed webhook signing secret couldn’t be resolved right now. Retry with backoff, or send an inline webhook.secret on the request instead.

SURFACE_TIMEOUT

504 — A sync capture didn’t complete in time. Retry, or submit async and poll the job.

INTERNAL_ERROR

500 — Unexpected server error. Retry with backoff; report the request_id if it persists. Full reference:
Retryable classes. The 429 (RATE_LIMIT_EXCEEDED, CONCURRENCY_LIMIT_EXCEEDED, QUEUE_CAPACITY_EXCEEDED), 502 (ACQUISITION_FAILED, UPSTREAM_BAD_GATEWAY), 503 (DRIVER_UNAVAILABLE, SUBMISSION_FAILED, WEBHOOK_SECRET_UNAVAILABLE), 504 (SURFACE_TIMEOUT), and 500 (INTERNAL_ERROR) codes are transient — retry them with exponential backoff and jitter (honor Retry-After when present). Every other code above is terminal for that request: fix the input, credits, scope, or id and resend.

Rate limiting & concurrency

All three limit conditions return HTTP 429. Every 429 carries the headers you need to back off precisely — sourced from the live admission bucket for your key, never fabricated:
Retry-After
seconds
How long to wait before retrying. Always honor this value.
X-RateLimit-Limit
number
Your ceiling for the current window.
X-RateLimit-Remaining
number
Requests left in the current window.
X-RateLimit-Reset
epoch seconds
When the window resets and Remaining refills.
Successful submits (and 4xx rejections on the submit path) carry the same X-RateLimit-* headers, plus X-Concurrency-Limit / X-Concurrency-Running / X-Concurrency-Queued, so you can pace yourself before you hit the wall. The three flavors call for slightly different responses:
  • RATE_LIMIT_EXCEEDED — you’re sending too fast. Wait Retry-After, then resume. Watch X-RateLimit-Remaining to pace yourself.
  • CONCURRENCY_LIMIT_EXCEEDED — too many sync requests are open at once. Reduce parallelism, or switch to ?mode=async and let jobs fan out server-side.
  • QUEUE_CAPACITY_EXCEEDED — the queue is briefly full. Back off with jitter and retry; this clears on its own.

Backing off

Retry 429 and 5xx responses with exponential backoff, capped, with jitter. When Retry-After is present, prefer it over your computed delay.
Always honor Retry-After, and set an idempotencyKey on writes. If a retry lands after the original request already succeeded, the same key + same body replays the original job (202 + Idempotent-Replayed: true) instead of starting a new one — so retries stay safe and never double-charge credits.
A different body under a previously used idempotencyKey returns 409 IDEMPOTENCY_CONFLICT. Generate a fresh key whenever the request payload changes.

What isn’t an error

A surface returning nothing is not an error. The job still reaches completed with provenance.surfacePresent: false, an empty answer, and a surface_absent warning — and an empty capture costs no credits. Handle it as data, not as a failure. See The Envelope for the full shape.

Job lifecycle

Terminal states, polling, and when to stop.

Webhooks

Skip polling entirely for fan-out jobs.