GET /v1/jobs/:id, you register a webhook on a search and we deliver the complete Envelope to your endpoint when 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. Setting a
webhook forces the async (
202) path, even for a single surface.Register a webhook
Add awebhook object to any POST /v1/search body.
The endpoint that receives deliveries. The destination is SSRF-guarded: only
http/https schemes are allowed, and private, loopback, link-local, and
metadata addresses are rejected at submission (and re-checked at delivery).
Use a public HTTPS URL.The shared secret used to sign each delivery with HMAC-SHA256. If you omit it,
deliveries are signed with your account’s active managed signing secret
(
whsec_…, created in the dashboard). Store the secret somewhere your
receiver can read it, and never expose it client-side.Delivery payload
Each delivery is an HTTPPOST with a JSON body shaped like this:
Delivery body
Event id, derived from the child’s deterministic call id (
evt_call_…). A
re-delivery of the same child result reuses the same id — deduplicate on it.job.<status> — the child’s terminal status, e.g. job.completed, job.partial, or job.failed.ISO-8601 timestamp of when the event was generated.
The child’s job header:
id, query, surface, region, status,
warnings, requestedAt, and completedAt.Success-only billing for this child:
{ creditsToCharge, creditsCharged }.
Present on the top-level payload and again inside result.The full Envelope for this child —
job,
provenance, answer, evidence, an optional html proof-of-page URL (only
when the submit opted in with include.html: true on a consumer-UI surface),
and credits — 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 signed delivery carries two headers:Unix time in seconds at which the delivery was signed. Use it to reject
stale or replayed deliveries.
HMAC-SHA256(secret, "${timestamp}.${body}"), encoded as lowercase hex.., then the exact raw request body bytes — ${timestamp}.${body}. Binding the
timestamp into the signature is what lets you detect replays.
To verify a delivery:
1
Read both headers
Grab
X-AISearch-Timestamp and X-AISearch-Signature. Reject the request
if either is missing.2
Bound the timestamp skew
Reject if
timestamp is not a number or differs from your current clock by
more than ~300 seconds. This is your replay defense.3
Recompute and compare
Compute
HMAC-SHA256(secret, "${timestamp}.${rawBody}") as lowercase hex
and compare it against X-AISearch-Signature with a timing-safe equality.A worked example
With this exact secret, timestamp, and body, the signed string is${timestamp}.${body} and you must compute this exact signature — use it to
unit-test your verifier:
Reproduce it
${timestamp}. to the raw
request bytes, recompute the HMAC, and compare it against the
X-AISearch-Signature header:
Delivery semantics
1
Return 2xx quickly
Respond as soon as you have verified and accepted the delivery. Do the heavy
work — storing the Envelope, updating your records — asynchronously. A
non-
2xx response (or a timeout) counts as a failed attempt and is retried.2
Treat deliveries as at-least-once
Deliveries are pushed from a durable workflow step, so the same event
id
can arrive more than once — after a retry, or if your 2xx was lost on the
wire. Deduplicate on the event id and make your handler idempotent so
a repeat delivery is a no-op.3
Expect retries with backoff
A failed attempt (network error, non-
2xx, or timeout) is re-run by the
durable engine with exponential backoff across several attempts. Because
retries reuse the same event id, your dedupe key covers them
automatically.4
Don't rely on retries as your only path
The result is always persisted regardless of delivery. If every attempt is
exhausted (for example your endpoint stayed down), reconcile by reading
GET /v1/jobs/:childId — the webhook payload’s result is exactly that body,
including its credits.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.Watch run deliveries
A Watch with awebhookUrl receives the same signed
delivery for every surface × region on each dispatched run. Event ids,
deduplication, retries, signature verification, and payload semantics are
identical to any other async job.
Related
The Envelope
The full result shape delivered in
result.Job lifecycle
When jobs reach the terminal states that trigger deliveries.
POST /v1/search
Submit a search and attach a webhook.
Watches
Schedule searches with the same signed delivery semantics.