The error model
Every error response — on every endpoint — has the same envelope:Error envelope
A stable, machine-readable identifier. Branch on this — never on
error.A human-readable, actionable explanation. Validation errors name the
offending field and the allowed values. Safe to log; may change over time.
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.A deep link to the failing code’s section of this page.
Optional — present on some
VALIDATION_FAILED responses with per-field
issues.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
422 — Reserved; 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 HTTP429. Every 429 carries the headers you need to back off precisely — sourced from the live admission bucket for your key, never fabricated:
How long to wait before retrying. Always honor this value.
Your ceiling for the current window.
Requests left in the current window.
When the window resets and
Remaining refills.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. WaitRetry-After, then resume. WatchX-RateLimit-Remainingto pace yourself.CONCURRENCY_LIMIT_EXCEEDED— too many sync requests are open at once. Reduce parallelism, or switch to?mode=asyncand 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
Retry429 and 5xx responses with exponential backoff, capped, with jitter. When Retry-After is present, prefer it over your computed delay.
What isn’t an error
A surface returning nothing is not an error. The job still reachescompleted 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.