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

# Webhooks

> Signed per the Standard Webhooks spec, retried for 24 hours, every attempt queryable. The integration path for backends that won't hold an SSE socket.

Webhooks deliver run terminals, proposals, monitor diffs, and balance warnings to your
backend. They are signed per the **Standard Webhooks spec** — any standard-webhooks
library verifies them with no custom code.

## Endpoints

Manage endpoints in the dashboard or via the API:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/webhooks \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.myapp.com/hooks/fivenine", "events": ["session.answered", "monitor.changed"]}'
```

The `201` response carries the endpoint's `whsec_…` secret — **this once**. Store it;
it cannot be read back.

Alternatively, pass a per-run `webhook_url` on session/extraction create. Ad-hoc URLs
sign with your **account default secret** — provisioned lazily on first use, readable
and rotatable in the dashboard's webhook settings (the one secret that isn't
shown-once, because ad-hoc URLs have no creation moment to show it at).

**Payload versioning:** each endpoint pins the `fn-version` current at its creation;
payloads serialize under that pin whatever your account pin does later. `PATCH` the
endpoint to advance it deliberately. Ad-hoc `webhook_url` deliveries use the account
pin.

**Delivery targets are SSRF-checked as a hard wall:** https only, hosts must resolve
to public unicast addresses — re-resolved and re-checked at every delivery attempt —
redirects are not followed, and private/loopback/link-local/metadata ranges are
refused with [invalid\_param](/errors/invalid_param).

## Delivery contract

Every delivery carries:

```
webhook-id: msg_7d20a1
webhook-timestamp: 1787724004
webhook-signature: v1,MEyCBz…
```

* Your endpoint must return 2xx within **10 seconds**. Do the work after you respond.
* Retries at 1 m, 5 m, 15 m, 1 h, 6 h, 24 h — then dead-lettered, visible in the
  attempts log.
* **At-least-once**: dedupe on `webhook-id`.
* Every attempt is kept and queryable: `GET /v1/webhooks/{id}/attempts`.
* `POST …/attempts/{msg}/redeliver` (**not yet available — planned**) will redeliver
  a specific message by hand.
* `POST /v1/webhooks/{id}/test` (**not yet available — planned**) will send a sample
  delivery of each subscribed event type on demand — signed, retried, and logged like
  a real one, with `"test": true` in the envelope, working on `fn_test_` keys. Until
  it ships, verify your delivery path by registering an endpoint and watching the
  attempts log on a real (or test-key replayed) run terminal.

## Verifying

Verify against the **raw** request body (before any JSON parsing middleware touches
it); reject timestamps older than 5 minutes.

```ts theme={null}
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.FN_WEBHOOK_SECRET);
const event = wh.verify(rawBody, headers); // throws on bad signature or stale timestamp
```

## Payload envelope

```json theme={null}
{
  "id": "msg_7d20a1",
  "type": "monitor.changed",
  "created": "2026-08-26T06:00:04Z",
  "data": { "…": "type-specific, below" }
}
```

Payloads are **notifications, not deliveries**: they carry summaries and ids — fetch
rows and results via the API (they can exceed webhook size budgets). The type catalog
is an open enum: dedupe on `id`, branch on `type`, ignore unknown types.

## Event catalog

### `session.answered`

```json theme={null}
{
  "session_id": "sess_9m2kd",
  "status": "answered",
  "turn": 1,
  "qualified": 41,
  "records": 228,
  "sources": 3,
  "receipts_signed": 3,
  "spend_usd": 0.94,
  "result_url": "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/result"
}
```

### `session.abstained` · `session.error` · `session.stopped`

```json theme={null}
{
  "session_id": "sess_2rr8x",
  "status": "error",
  "reason": "engine failure after 2 deliveries",
  "settled_usd": 0.12,
  "released_usd": 0.88,
  "records": 74
}
```

The money statement rides along: what settled (backed by signed receipts) and what was
released back to your balance. `stopped` additionally carries `stop`
(`spend-budget` | `query-budget` | `stopped`).

### `session.proposal`

```json theme={null}
{
  "session_id": "sess_9m2kd",
  "turn": 4,
  "proposal": { "estimated_usd": 1.2, "passes": 3, "expires_at": "2026-08-27T15:32:07Z" }
}
```

A follow-up turn needs confirmation — see
[Budgets and proposals](/guides/proposals).

### `extraction.delivered` · `extraction.failed`

```json theme={null}
{
  "extraction_id": "ext_6b1st",
  "url": "powellsbooks.com/used/poetry",
  "status": "delivered",
  "records": 214,
  "claim": "complete",
  "receipt_id": "rcpt_p11xa",
  "spend_usd": 0.48
}
```

### `monitor.changed` · `monitor.unchanged` · `monitor.failed`

```json theme={null}
{
  "monitor_id": "mon_5wp0d",
  "run_id": "run_0pw3e",
  "added": 9,
  "removed": 2,
  "price_moves": 3,
  "rows": 221,
  "cost_usd": 0.31,
  "receipt_id": "rcpt_p88wz"
}
```

`unchanged` deliveries are opt-in per endpoint (default: change-only). `failed`
carries `reason` and `consecutive_failures`; after 3 consecutive failures the monitor
pauses itself and sends a final `monitor.failed` with `"paused": true`.

### `balance.low`

```json theme={null}
{
  "balance_usd": 8.6,
  "threshold_usd": 10,
  "open_reserves_usd": 1,
  "auto_recharge": { "enabled": true, "amount_usd": 25 }
}
```

Fires when the balance crosses your account's threshold — **before** the
[402](/errors/insufficient_credit), not after.

## Local development

There is no first-party tunnel tool, and delivery targets must be public https (the
SSRF wall refuses `localhost`). Use a tunnel of your choice (cloudflared, ngrok) to
receive real deliveries locally. `POST /v1/webhooks/{id}/test` for CI is planned but
not yet available — until it ships, drive a test-key replay run to a terminal and
verify your handler against the real delivery it produces.
