Every error the API returns shares one 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 has the same envelope:
Error envelope
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request body failed validation.",
    "status": 400,
    "details": [
      { "path": "surfaces", "message": "At least one surface is required." },
      { "path": "query", "message": "query must be 1–10000 characters." }
    ]
  }
}
error.code
string
A stable, machine-readable identifier. Branch on this — never on message.
error.message
string
A human-readable explanation. Safe to log; may change over time.
error.status
number
The HTTP status code, mirrored into the body for convenience.
error.details
array
Present on VALIDATION_FAILED. Each entry names the offending path and what went wrong.
Every response — success or error — also carries an X-AISearch-Version header so you can pin behavior to a known API version.

Error codes

AUTH_MISSING
401
No Authorization header was sent. Add Authorization: Bearer <API_KEY>.
AUTH_INVALID
401
The API key is malformed, revoked, or unknown.
VALIDATION_FAILED
400
The request body is invalid. Inspect details[] for the specific fields.
UNSUPPORTED_SURFACE
400
A value in surfaces isn’t a supported surface. See the surfaces enum.
IDEMPOTENCY_CONFLICT
409
The same idempotencyKey was reused with a different body. Use a new key or resend the original body.
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, or use the async default.
QUEUE_CAPACITY_EXCEEDED
429
The job queue is temporarily saturated. Back off and retry.
JOB_NOT_FOUND
404
The job id doesn’t exist (or isn’t yours). Check the id and that it hasn’t expired.
SURFACE_TIMEOUT
504
A sync capture didn’t complete in time. Retry, or submit async and poll the job.
Full reference:
CodeHTTPWhen it happens
AUTH_MISSING401No Authorization header.
AUTH_INVALID401Key is malformed, revoked, or unknown.
VALIDATION_FAILED400Body failed validation; see details[].
UNSUPPORTED_SURFACE400A surfaces value isn’t supported.
IDEMPOTENCY_CONFLICT409Same idempotencyKey, different body.
RATE_LIMIT_EXCEEDED429Request rate exceeded.
CONCURRENCY_LIMIT_EXCEEDED429Too many concurrent sync requests.
QUEUE_CAPACITY_EXCEEDED429Job queue temporarily saturated.
JOB_NOT_FOUND404Unknown or expired job id.
SURFACE_TIMEOUT504Sync capture didn’t finish in time.

Rate limiting & concurrency

All three limit conditions return HTTP 429. Every 429 carries the headers you need to back off precisely:
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.
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 before you hit the wall.
  • CONCURRENCY_LIMIT_EXCEEDED — too many sync requests are open at once. Reduce parallelism, or switch to the async default 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.
# Retry on 429/5xx, honoring Retry-After
url="https://api.aisearchapi.dev/v1/search"
for attempt in 1 2 3 4 5; do
  resp=$(curl -s -w "\n%{http_code}" -X POST "$url" \
    -H "Authorization: Bearer $AISEARCH_API_KEY" \
    -H "Content-Type: application/json" \
    -D /tmp/headers.txt \
    -d '{"query":"best crm for startups","surfaces":["chatgpt"]}')
  code=$(printf '%s' "$resp" | tail -n1)
  [ "$code" -lt 429 ] && { printf '%s\n' "$resp" | sed '$d'; break; }
  retry=$(grep -i '^retry-after:' /tmp/headers.txt | tr -d '\r' | awk '{print $2}')
  sleep "${retry:-$((2 ** attempt))}"
done
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 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.