> ## Documentation Index
> Fetch the complete documentation index at: https://webscrape.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Push delivery for SmartBrowse run lifecycle events.

Add a delivery URL to a SmartBrowse recipe and we'll `POST` to it when a run finishes. Webhooks let you skip the poll loop entirely.

## Configure a webhook

Open the recipe in the [SmartBrowse studio](https://webscrape.ai/app/smartbrowse), fill in the **Webhook URL** field, and (optionally) add custom request headers — for example an `Authorization` header your endpoint checks on receipt.

You can also limit which events fire by listing them in the recipe config. Leave the list empty to receive everything.

## Event names

`event` is one of:

| Event       | When                                                                |
| ----------- | ------------------------------------------------------------------- |
| `completed` | Run finished and items came back.                                   |
| `failed`    | Run errored out.                                                    |
| `cancelled` | Run was force-stopped (e.g. an account deletion confirmed mid-run). |

## Payload shape

Each delivery is a `POST` with `Content-Type: application/json` and a flat JSON body — **not** the API envelope. Shape:

```json theme={null}
{
  "event": "completed",
  "timestamp": "2026-05-21T17:04:12Z",
  "run_id": "k7Xb9dRmQ2p",
  "recipe_id": "m3Yc2tFvN8q",
  "status": "completed",
  "pages_extracted": 5,
  "items_extracted": 87,
  "credits_used": 10,
  "mode": "replay",
  "drift": 0.02,
  "warnings": [],
  "error": ""
}
```

`failed` and `cancelled` deliveries use the same shape, with `status` set accordingly and `error` populated:

```json theme={null}
{
  "event": "failed",
  "timestamp": "2026-05-21T17:10:00Z",
  "run_id": "p9Lk3vWqR7s",
  "recipe_id": "m3Yc2tFvN8q",
  "status": "failed",
  "pages_extracted": 0,
  "items_extracted": 0,
  "credits_used": 0,
  "mode": "",
  "drift": 0,
  "warnings": [],
  "error": "target page did not load within timeout"
}
```

## Authenticating deliveries

We don't sign deliveries yet, so the burden's on your handler to confirm the request really came from us. Two ways to do it:

* Use a hard-to-guess URL with a random path segment your endpoint expects (`/webhooks/wsg/8x4Kpz...`).
* Add a custom header — `Authorization: Bearer <your-token>` or anything you want — to the recipe's webhook config, and check it on every request.

Pick one or both. Either way, verification stays in your handler.

<Warning>
  Until we ship signed deliveries, treat the body as untrusted until you've validated the URL secret or header. Don't act on `run_id` from an unauthenticated POST.
</Warning>

## Delivery semantics

Every terminal event gets exactly one attempt — no automatic retries. We give your handler 10 seconds before we give up, and anything that returns a non-2xx is logged on our side but never redelivered. Treat it as fire-and-forget.

If push delivery is critical to you, treat the webhook as a hint and fall back to polling `GET /v1/smartbrowse/runs/{id}` on a long interval (every few minutes) to pick up anything you missed.

## Testing locally

Point [ngrok](https://ngrok.com) or [webhook.site](https://webhook.site) at a handler to expose it to the public internet. Trigger a run from the dashboard and watch the delivery land.
