Webhooks

How async endpoints deliver finished runs to your server.
View as Markdown

Scout’s asynchronous endpoints can POST the finished payload to a URL you control instead of asking you to poll. The shape of the payload is the same as the GET response for that run, so a webhook handler is usually a few lines.

Async endpoints that accept a webhook field:

  • /v1/task (research and other processor tiers)
  • /v1/search?depth=deep
  • /v1/findall
  • /v1/monitors (one webhook per monitor, fires on every emitted event)

Setting it up

Pass an absolute https:// URL when you create the run:

1{
2 "task": "Compare Pinecone, Weaviate, and Qdrant for hybrid search",
3 "processor": "research",
4 "webhook": "https://api.yourapp.com/scout/task-complete"
5}

The endpoint must respond 2xx within 15 seconds. Slower responses count as failures and trigger a retry.

What gets POSTed

The body is the same view the polling endpoint would have returned — plus a delivery_id you can use for idempotency.

1{
2 "delivery_id": "wh_8c4f...",
3 "task_id": "run-abc123",
4 "status": "completed",
5 "processor": "research",
6 "output": {
7 "report": "# ...",
8 "findings": [...],
9 "sources": [...]
10 },
11 "usage": { ... },
12 "metadata": { "caller": "your-app" },
13 "completed_at": "2026-06-04T12:34:56Z"
14}

Content-Type is application/json. Charset is UTF-8.

Verifying the signature

When you set webhook_secret on a monitor (or on any other webhook- capable run), every outbound POST carries an HMAC of the raw body in X-Scout-Signature.

HeaderDescription
X-Scout-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body, keyed by your webhook_secret.
X-Scout-TimestampUnix seconds at the moment we sent the request.
X-Scout-EventThe event type, e.g. task.completed, monitor.changed.

Compare the header against your own HMAC of the body. If they do not match, reject the request — it was not sent by Scout.

1import hmac, hashlib
2
3def verify(request, secret: str) -> bool:
4 received = request.headers.get("X-Scout-Signature", "")
5 if not received.startswith("sha256="):
6 return False
7 expected = "sha256=" + hmac.new(
8 secret.encode(), request.body, hashlib.sha256
9 ).hexdigest()
10 return hmac.compare_digest(received, expected)
1import { createHmac, timingSafeEqual } from "node:crypto";
2
3function verify(rawBody: Buffer, header: string, secret: string): boolean {
4 if (!header.startsWith("sha256=")) return false;
5 const expected =
6 "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
7 return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
8}

The signature is computed on the raw bytes, before any JSON parsing or encoding transformation your framework might apply. If you cannot get the raw body, the verification will fail — many frameworks have a way to opt into raw-body capture for specific routes.

Retries

Scout retries non-2xx responses with exponential backoff. The default policy is three attempts at 30, 60, and 120 seconds; you can override it on monitors via webhook_retry.

FieldDefaultRangeNotes
webhook_retry.max_attempts315Including the first attempt.
webhook_retry.backoff_seconds301600Doubled after each failure.

A run’s webhook_attempts, last_webhook_status, and last_webhook_error fields tell you how delivery went.

Idempotency

Because of retries, your endpoint must be safe to call more than once for the same logical event. Use the delivery_id as the dedup key — it is stable across retry attempts for the same delivery, but unique between distinct deliveries.

1if already_processed(delivery_id):
2 return 200
3process_payload(body)
4mark_processed(delivery_id)
5return 200

Webhook health

On monitors, the dashboard surfaces last_webhook_status, last_webhook_at, and last_webhook_error so you can see at a glance when deliveries started failing. A monitor whose webhook has been failing for more than 24 hours is flagged in the UI.

Testing locally

For local development, point the webhook at a tunnel like ngrok or Cloudflare Tunnel. A direct localhost URL will not work — Scout cannot reach it.

If you do not want to expose a public endpoint at all, webhook.site gives you a one-off URL that captures incoming requests and shows their headers and body. Useful for verifying the payload shape and signature flow before you write your handler.