Every search you submit becomes a job. Because a single request can span multiple AI surfaces across multiple regions, the API is async by default: you get an immediate acknowledgment, and the actual captures run in the background from a real browser.

The async model

POST /v1/search returns 202 Accepted right away with a parent job. The parent fans out into one child per surface × region — that’s the unit of work that actually runs a capture and produces an Envelope.
curl -X POST https://api.aisearchapi.dev/v1/search \
  -H "Authorization: Bearer $AISEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "best crm for startups",
    "surfaces": ["chatgpt", "perplexity"],
    "regions": [{ "country": "US" }, { "country": "GB" }]
  }'
The 202 response names the parent and lists its children — here, 2 surfaces × 2 regions = 4 children:
{
  "jobId": "job_8t2q",
  "status": "processing",
  "children": [
    "job_8t2q.chatgpt.us",
    "job_8t2q.chatgpt.gb",
    "job_8t2q.perplexity.us",
    "job_8t2q.perplexity.gb"
  ]
}
The response also carries a Location header pointing at the parent job, and an X-AISearch-Version header. For a single surface where you’d rather block and get the result inline, use ?mode=sync (or the Prefer: wait header) — see Sync vs async.

Parent vs child ids

The shape of a job id tells you what you’ll get back when you read it.
Parent id
no dots — e.g. job_8t2q
Resolves to the fan-out summary: { job, children[] }. Use it to see overall status and enumerate children.
Child id
dotted — e.g. job_8t2q.chatgpt.us
Shaped job_<id>.<surface>.<region>. Resolves to the canonical Envelope — the answer plus the structured evidence — for exactly one surface in one region.
Both are read through the same endpoint, GET /v1/jobs/:id. The id you pass decides which shape comes back.

Lifecycle states

StateKindMeaning
queuedActiveAccepted, waiting to run.
processingActiveA capture is running (or children are still in flight).
completedTerminalFinished successfully.
partialTerminalParent only — some children completed, some failed.
failedTerminalDid not produce a result.
canceledTerminalStopped before completion.
expiredTerminalAged out before running.
A polling loop must stop on any terminal state — not just completed. Treat completed, partial, failed, canceled, and expired as “done.”

Parent rollup rules

A parent’s status is derived from its children:
  • completed — every child completed.
  • failed — every child failed.
  • partial — a mix of completed and failed children.
While any child is still queued or processing, the parent stays processing.
A child can complete even when the surface returned nothing. In that case the Envelope has provenance.surfacePresent: false, an empty answer, and a surface_absent warning — the job is still completed, and an empty capture is not charged. See The Envelope.

How to read results

You have two ways to collect results after the 202.
1

Poll the parent for status

GET /v1/jobs/job_8t2q returns the current parent status and every child’s status. Stop as soon as the parent reaches a terminal state.
2

Read each child Envelope

For every child that reached completed (or partial’s completed children), GET /v1/jobs/job_8t2q.chatgpt.us to fetch its full Envelope.
A parent GET looks like this:
{
  "job": { "id": "job_8t2q", "status": "processing" },
  "children": [
    { "id": "job_8t2q.chatgpt.us", "surface": "chatgpt", "region": "US", "status": "completed" },
    { "id": "job_8t2q.chatgpt.gb", "surface": "chatgpt", "region": "GB", "status": "completed" },
    { "id": "job_8t2q.perplexity.us", "surface": "perplexity", "region": "US", "status": "completed" },
    { "id": "job_8t2q.perplexity.gb", "surface": "perplexity", "region": "GB", "status": "processing" }
  ]
}
Then read a finished child:
curl https://api.aisearchapi.dev/v1/jobs/job_8t2q.chatgpt.us \
  -H "Authorization: Bearer $AISEARCH_API_KEY"

A minimal poll loop

Node
async function waitForJob(parentId) {
  const terminal = ["completed", "partial", "failed", "canceled", "expired"];
  while (true) {
    const res = await fetch(`https://api.aisearchapi.dev/v1/jobs/${parentId}`, {
      headers: { Authorization: `Bearer ${process.env.AISEARCH_API_KEY}` },
    });
    const { job, children } = await res.json();
    if (terminal.includes(job.status)) return children;
    await new Promise((r) => setTimeout(r, 2000));
  }
}
For fan-out — many surfaces, many regions — prefer webhooks over polling. Register webhook: { url, secret } on the search and you’ll receive an HMAC-signed POST as each child reaches its terminal state, with the full Envelope in the payload. It’s fewer requests and lower latency than a poll loop. See Webhooks.

The Envelope

The canonical shape a child job resolves to.

Webhooks

Get pushed each child’s result instead of polling.