> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.usescout.sh/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.usescout.sh/_mcp/server.

# Webhooks

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:

```json
{
  "task": "Compare Pinecone, Weaviate, and Qdrant for hybrid search",
  "processor": "research",
  "webhook": "https://api.yourapp.com/scout/task-complete"
}
```

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.

```json
{
  "delivery_id": "wh_8c4f...",
  "task_id": "run-abc123",
  "status": "completed",
  "processor": "research",
  "output": {
    "report": "# ...",
    "findings": [...],
    "sources": [...]
  },
  "usage": { ... },
  "metadata": { "caller": "your-app" },
  "completed_at": "2026-06-04T12:34:56Z"
}
```

`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`.

| Header              | Description                                                                           |
| ------------------- | ------------------------------------------------------------------------------------- |
| `X-Scout-Signature` | `sha256=<hex>` — HMAC-SHA256 of the raw request body, keyed by your `webhook_secret`. |
| `X-Scout-Timestamp` | Unix seconds at the moment we sent the request.                                       |
| `X-Scout-Event`     | The 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.

```python
import hmac, hashlib

def verify(request, secret: str) -> bool:
    received = request.headers.get("X-Scout-Signature", "")
    if not received.startswith("sha256="):
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), request.body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(received, expected)
```

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: Buffer, header: string, secret: string): boolean {
  if (!header.startsWith("sha256=")) return false;
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
```

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`.

| Field                           | Default | Range     | Notes                        |
| ------------------------------- | ------- | --------- | ---------------------------- |
| `webhook_retry.max_attempts`    | `3`     | `1`–`5`   | Including the first attempt. |
| `webhook_retry.backoff_seconds` | `30`    | `1`–`600` | Doubled 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.

```python
if already_processed(delivery_id):
    return 200
process_payload(body)
mark_processed(delivery_id)
return 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](https://ngrok.com) or [Cloudflare Tunnel](https://www.cloudflare.com/products/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](https://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.