Webhooks are the recommended way to consume results from fan-out searches. Instead of polling GET /v1/jobs/:id, you register a webhook on a search and we deliver the complete Envelope to your endpoint the moment each child job reaches a terminal state.
One child is created per surface × region. You receive one webhook delivery per child, each carrying that child’s full Envelope. Prefer webhooks over polling whenever you request more than one surface or region.

Register a webhook

Add a webhook object to any POST /v1/search body. Both fields are required.
webhook.url
string
required
The HTTPS endpoint that receives deliveries. The destination is SSRF-guarded: only http/https schemes are allowed, and private, loopback, and link-local addresses are rejected. Use a public HTTPS URL.
webhook.secret
string
required
The shared secret used to sign each delivery with HMAC-SHA256. Store it somewhere your receiver can read it, and never expose it client-side.
curl 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" }],
    "webhook": {
      "url": "https://example.com/hooks/aisearch",
      "secret": "whsec_your_signing_secret"
    }
  }'
Signing secrets can be created, rotated, and revoked under /v1/webhooks/secrets. Rotate a secret if you suspect it has leaked; deliveries signed with the old secret keep verifying until you revoke it.

Delivery payload

Each delivery is an HTTP POST with a JSON body shaped like this:
Delivery body
{
  "id": "evt_9b3c1a8e",
  "type": "job.completed",
  "createdAt": "2026-06-30T17:02:23Z",
  "job": {
    "id": "job_8t2q.chatgpt.us",
    "surface": "chatgpt",
    "status": "completed"
  },
  "result": {
    "job": {
      "id": "job_8t2q.chatgpt.us",
      "query": "best crm for startups",
      "surface": "chatgpt",
      "region": "US",
      "status": "completed",
      "warnings": [],
      "requestedAt": "2026-06-30T17:02:11Z",
      "completedAt": "2026-06-30T17:02:23Z"
    },
    "provenance": {
      "model": { "providerId": "openai", "observedLabel": "GPT-5", "inferred": false, "confidence": 0.98 },
      "webSearch": { "enabled": true, "known": true },
      "region": { "requested": "US", "effective": "US" },
      "surfacePresent": true
    },
    "answer": {
      "text": "For startups, the top CRMs are HubSpot, Attio and Pipedrive...",
      "markdown": "For startups, the top CRMs are **HubSpot**, **Attio** and **Pipedrive**...",
      "blocks": [{ "type": "paragraph", "text": "...", "referenceIds": [1] }]
    },
    "evidence": {
      "sources": [
        { "id": 1, "url": "https://...", "title": "Best CRMs for startups", "role": "cited", "cited": true, "charRanges": [[0, 58]], "quote": "..." }
      ],
      "fanOut": { "queries": ["best crm for startups 2026", "hubspot vs attio"] },
      "mentions": ["HubSpot", "Attio", "Pipedrive"],
      "shopping": [],
      "ads": []
    }
  }
}
id
string
Unique event id (e.g. evt_9b3c1a8e). Use it to deduplicate deliveries, since retries reuse the same id.
type
string
The event type, one of job.completed, job.partial, or job.failed. This mirrors the child’s terminal status.
createdAt
string
ISO-8601 timestamp of when the event was generated.
job
object
A compact reference to the child job: id (the dotted child id), surface, and status.
result
object
The full Envelope for this child — the same body returned by GET /v1/jobs/:childId.
A child can complete with nothing to report: if the surface returned no answer, the Envelope carries provenance.surfacePresent: false, a surface_absent warning, and an empty answer. The delivery type is still job.completed. Handle this as a normal, successful outcome.

Verify the signature

Every delivery is signed with HMAC-SHA256 over the exact raw request bytes, encoded as lowercase hex, in the X-AISearch-Signature header.
Verify the signature against the raw body before parsing JSON. Compute the HMAC over the bytes you received, not over a re-serialized object — any reformatting changes the signature. Always compare using a timing-safe function.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SECRET = process.env.AISEARCH_WEBHOOK_SECRET;

function verify(rawBody, header, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

// Capture the raw bytes — do NOT use express.json() on this route.
app.post(
  "/hooks/aisearch",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-AISearch-Signature");

    if (!verify(req.body, signature, SECRET)) {
      return res.status(401).send("invalid signature");
    }

    const event = JSON.parse(req.body.toString("utf8"));

    // Acknowledge fast, then process out of band.
    res.status(200).send("ok");
    queueForProcessing(event); // e.g. push to a job queue
  }
);

app.listen(3000);

Retries and acknowledgment

1

Return 2xx quickly

Respond with any 2xx status as soon as you have verified and accepted the delivery. Do the heavy work — storing the Envelope, updating your records — asynchronously. A slow handler risks a timeout, which we treat as a failed delivery.
2

Non-2xx triggers a retry

Any non-2xx response (or a timeout) is considered a failure, and the delivery is retried with backoff. Retries reuse the same event id, so deduplicate on it to stay idempotent.
3

Deduplicate on event id

Because a delivery can arrive more than once, treat processing as at-least-once. Record each id you have handled and skip duplicates.
The delivery destination is validated for SSRF on every attempt: only http/https URLs are permitted, and requests to private, loopback, and link-local addresses are refused. Point your webhook at a publicly reachable HTTPS endpoint.

The Envelope

The full result shape delivered in result.

Webhook secrets

Create, rotate, and revoke signing secrets.

Job lifecycle

When jobs reach the terminal states that trigger deliveries.

POST /v1/search

Submit a search and attach a webhook.