There is no official SDK yet. The AI Search API is plain HTTP with JSON bodies and a Bearer token, so any HTTP client works — curl, fetch, requests, or your language’s standard library. The clients below are unofficial, community examples. They’re pinned to the public wire contract (POST /v1/search → poll GET /v1/jobs/:id → read each child Envelope) and are meant to be copied into your project and adapted. Nothing here needs a dependency beyond your language’s standard HTTP tooling.
Set your key once in the environment:
export AISEARCH_API_KEY="sk_live_..."

What these clients do

Both clients follow the same shape:
1

Submit

POST /v1/search with a query and one or more surfaces. This returns 202 with a parent job id and a children[] list — one child per surface × region.
2

Poll to terminal

GET /v1/jobs/:parentId until the parent reaches a terminal state (completed, partial, failed, canceled, or expired). On 429, back off using Retry-After.
3

Read each child

For every child, GET /v1/jobs/:childId to fetch the canonical Envelopeanswer, evidence, and provenance for that one surface.
For fan-out at scale, prefer webhooks over polling — register webhook: { url, secret } on the search and receive each child’s Envelope on its terminal state instead of looping.

Clients

// Unofficial community client — Node.js 18+, zero dependencies.
// Pinned to the public wire contract: submit -> poll parent -> read children.

const BASE_URL = "https://api.aisearchapi.dev";
const API_KEY = process.env.AISEARCH_API_KEY;

const TERMINAL = new Set([
  "completed",
  "partial",
  "failed",
  "canceled",
  "expired",
]);

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function request(path, init = {}, attempt = 0) {
  const res = await fetch(`${BASE_URL}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
      ...(init.headers ?? {}),
    },
  });

  // Respect rate limits: back off on 429 using Retry-After, then retry.
  if (res.status === 429 && attempt < 6) {
    const retryAfter = Number(res.headers.get("Retry-After") ?? 1);
    await sleep((Number.isFinite(retryAfter) ? retryAfter : 1) * 1000);
    return request(path, init, attempt + 1);
  }

  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    const err = body.error ?? {};
    throw new Error(
      `${res.status} ${err.code ?? "REQUEST_FAILED"}: ${err.message ?? res.statusText}`,
    );
  }

  return res.json();
}

// 1) Submit a search. Returns { jobId, status, children: [...] }.
async function submit(body) {
  return request("/v1/search", {
    method: "POST",
    body: JSON.stringify(body),
  });
}

// 2) Poll the parent job until it reaches a terminal state.
async function waitForParent(parentId, { intervalMs = 2000, timeoutMs = 120000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const { job, children } = await request(`/v1/jobs/${parentId}`);
    if (TERMINAL.has(job.status)) return { job, children };
    await sleep(intervalMs);
  }
  throw new Error(`Timed out waiting for ${parentId}`);
}

// 3) Read one child's Envelope (answer + evidence + provenance).
async function readChild(childId) {
  return request(`/v1/jobs/${childId}`);
}

async function search(body, opts) {
  const { jobId } = await submit(body); // browser-first by default
  const { job, children } = await waitForParent(jobId, opts);
  const envelopes = await Promise.all(children.map((c) => readChild(c.id)));
  return { job, envelopes };
}

// Example
const { job, envelopes } = await search({
  query: "best crm for startups",
  surfaces: ["chatgpt", "perplexity"],
  regions: [{ country: "US" }],
});

console.log(`parent ${job.id}: ${job.status}`);
for (const env of envelopes) {
  console.log(`\n[${env.job.surface} / ${env.job.region}] ${env.job.status}`);
  console.log(env.answer.text.slice(0, 160));
}
A child id is dotted — for example job_8t2q.chatgpt.us — while a parent id has no dots (job_8t2q). The clients above never construct child ids by hand; they use the ids the parent job hands back in children[].

Handling empty surfaces

A child can complete with no answer. When a surface returns nothing, that child is still completed with provenance.surfacePresent: false, a surface_absent warning, and an empty answer. Treat it as a normal terminal result, not an error:
for (const env of envelopes) {
  if (!env.provenance.surfacePresent) {
    console.log(`${env.job.surface}: surface returned nothing`);
    continue;
  }
  console.log(`${env.job.surface}: ${env.answer.text}`);
}

Sync shortcut

If you only need one surface and want the Envelope inline, skip polling entirely. Add ?mode=sync (or send the header Prefer: wait) and the request returns 200 with the full Envelope in the response body instead of a 202 parent job:
curl -s "https://api.aisearchapi.dev/v1/search?mode=sync" \
  -H "Authorization: Bearer $AISEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "best crm for startups",
    "surfaces": ["chatgpt"],
    "regions": [{ "country": "US" }]
  }'
The convenience alias POST /v1/search/:surface (e.g. /v1/search/chatgpt) is sync-by-default for a single surface and accepts prompt (an alias of query) plus a flat country:
cURL
curl -s "https://api.aisearchapi.dev/v1/search/chatgpt" \
  -H "Authorization: Bearer $AISEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "best crm for startups", "country": "US" }'
Sync mode captures one surface at a time and can return CONCURRENCY_LIMIT_EXCEEDED (429) under load. For multiple surfaces or regions, submit async and poll — or use webhooks — rather than firing many sync calls in parallel.

Next steps

The Envelope

Every field these clients read back, section by section.

Webhooks

Skip polling for fan-out: get each child Envelope pushed to you on terminal state.

Errors

Every error code, including the 429s the clients back off on.

Search endpoint

Full request body, mode=sync, and idempotency.