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

# Overview

> Receive real-time notifications when tasks, sessions and identities complete, fail, or need human input.

Anchor delivers webhook events to your endpoint the moment something interesting happens - a task completes, a login fails, the agent needs human input. Each delivery is signed with HMAC-SHA256 so you can verify it came from Anchor and not someone spoofing your endpoint.

## Quick Start

### 1. Create a webhook

From the dashboard, open **Webhooks** under the Account section and click **Add webhook**:

* Paste your HTTPS endpoint (`https://your-app.example.com/anchor/webhooks`).
* Pick the events you want to subscribe to. See [Events](/webhooks/events) for the full catalog.
* Click **Create webhook**.

Anchor shows the **signing secret exactly once**. Copy it now - in order to verify incoming requests, we do NOT show it again. If you lose it, click **Rotate signing secret** to issue a new one (the old secret remains valid for 24 hours).

You can also create webhooks via the API:

```bash theme={null}
curl -X POST "https://api.anchorbrowser.io/v1/webhooks" \
  -H "anchor-api-key: $ANCHOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/anchor/webhooks",
    "description": "Production task notifications",
    "subscribed_events": ["task.completed", "task.failed", "task.healed"]
  }'
```

The response includes the `secret` field - store it, it never appears in any subsequent API response.

### 2. Receive events

Each event arrives as a `POST` with a JSON body and a few `Anchor-*` headers:

```http theme={null}
POST /anchor/webhooks
Host: your-app.example.com
User-Agent: Anchor-Webhooks/1.0
Content-Type: application/json
Anchor-Event: task.completed
Anchor-Delivery-Id: <uuid>
Anchor-Timestamp: 1716544084
Anchor-Signature: t=1716544084,v1=<hex hmac sha256>

{
  "id": "evt_01HXJ4...",
  "type": "task.completed",
  "created": "2026-05-24T12:18:05.123Z",
  "project_id": "5d2c31f6-ab7e-481a-b6fd-8b4a96a4e197",
  "text": "✅ Task `tsk_4f8w9n2b` completed in 4.2s",
  "data": { "...": "see Events for per-type payloads" }
}
```

**Always respond with a `2xx` status code within 10 seconds** - that's how Anchor knows the event was delivered. Anything else triggers a retry (see [Retries](#retries-and-delivery-guarantees)).

### 3. Verify the signature

Compute an HMAC-SHA256 of `v0:{Anchor-Timestamp}:{raw_body_bytes}` using your signing secret and compare it to the `v1=` value in the `Anchor-Signature` header in constant time. Reject anything you can't verify.

Full code examples (Node.js, Python, Go) are on the [Signature verification](/webhooks/signature-verification) page.

## Slack integration

Every event includes a top-level `text` field with a one-line human-readable summary. That's the same shape Slack incoming webhooks expect, so you can register a Slack URL as a webhook receiver with no relay:

1. In Slack, create an [Incoming Webhook](https://api.slack.com/messaging/webhooks) for the channel you want events posted to. Slack gives you a URL like `https://hooks.slack.com/services/T.../B.../...`.
2. In Anchor, **Webhooks → Add webhook**, paste the Slack URL, pick the events.
3. Done. Each delivery posts a message in the channel — for example:

   > ✅ Task `tsk_4f8w9n2b` completed in 4.2s

For richer formatting (blocks, mentions, attachments) write a small relay that reads `data` and posts the message you want; the structured fields stay machine-readable while `text` covers the simple case.

## Testing your endpoint

The dashboard's **Send test event** button fires a synthetic event of the type you choose through the real signing + retry path. Use it during development:

1. Pick the event type from the dropdown next to the payload preview.
2. Click **Send test event**.
3. Inspect the request at your endpoint.

The test delivery counts toward your delivery log so you can verify retry behaviour by intentionally returning `503` from your endpoint.

You can also trigger via the API:

```bash theme={null}
curl -X POST "https://api.anchorbrowser.io/v1/webhooks/<webhook_id>/test" \
  -H "anchor-api-key: $ANCHOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "task.completed" }'
```

## Retries and delivery guarantees

Anchor retries failed deliveries with exponential backoff:

| Attempt     | Time since first attempt | Schedule (after the previous) |
| ----------- | ------------------------ | ----------------------------- |
| 1 (initial) | 0 s                      | immediate                     |
| 2           | 5 s                      | +5 s                          |
| 3           | 15 s                     | +10 s                         |
| 4           | 30 s                     | +15 s                         |
| 5           | 50 s                     | +20 s                         |
| 6           | 80 s                     | +30 s                         |

A delivery is **retried** when:

* The HTTP request errors (DNS failure, timeout, TLS handshake failure).
* Your endpoint returns `5xx`, `408`, or `429`.

A delivery is **not retried** when your endpoint returns any other `4xx`. Use that intentionally for events you've already processed (`409 Conflict` after de-dupe is a common pattern).

After 6 attempts (\~80 s elapsed since the first attempt), the delivery is marked `dead` and won't be retried automatically. You can still see it in **Webhooks → Events** in the dashboard for 7 days.

**At-least-once delivery.** Because we retry on transient errors, your endpoint may receive the same event more than once. **Always** de-dupe by `id` (the envelope's top-level `id`, not `data.execution_id`).

## Idempotency and ordering

* Each delivery has a unique top-level `id` (`evt_...`). The same logical event will always carry the same `id`, so storing `(id, status)` in your database and checking before processing is enough to make your handler idempotent.
* Anchor does **not** guarantee delivery order. A retry may arrive after a later event for the same task. Use the `created` timestamp to order events on your side if you care about sequencing.
* An event delivered to multiple webhook endpoints carries the **same** `id` to every endpoint, so you can correlate deliveries across consumers.

## Inspecting deliveries

Every delivery - succeeded, failed, or dead - is visible in the dashboard's **Webhooks → Events** view for 7 days. The same data is available via the API:

```bash theme={null}
curl "https://api.anchorbrowser.io/v1/webhooks/<webhook_id>/events?status=failed&limit=25" \
  -H "anchor-api-key: $ANCHOR_API_KEY"
```

Each row shows `event_type`, `status`, `attempt`, `response_status`, `error_message`, `scheduled_at`, and `completed_at`. Use this to debug failing endpoints or to confirm a missed event made it through.

## Sensitive data and what we never send

Anchor sanitizes every webhook payload before it leaves our infrastructure:

1. **Whitelist via schema.** Only fields explicitly declared in each event's schema are sent. Anything else is stripped.
2. **Schema-driven secret redaction.** Task inputs marked `secret` in your task schema are masked as `*****` before they appear in `output.value`.
3. **Free-text scrubbing.** Error messages and self-heal explanations are scanned for credential patterns and masked.

What is **never** in any payload:

* Identity credential values (passwords, API tokens).
* Raw `inputs` from task executions.

## Best practices

<AccordionGroup>
  <Accordion title="Verify the signature on every request">
    Even if you trust your network perimeter - Anchor's signature is a proof that the request really came from us.
  </Accordion>

  <Accordion title="Acknowledge fast, work later">
    Return `2xx` within 10 seconds. If processing takes longer (e.g. you write to a slow downstream system), enqueue the event in your own queue and process it asynchronously.
  </Accordion>

  <Accordion title="De-dupe by `id`">
    Anchor delivers at-least-once. Persist `(id, status)` in your database and short-circuit duplicates with a `200 OK`.
  </Accordion>

  <Accordion title="Rotate secrets without downtime">
    Click **Rotate signing secret** to get a new one. The old secret remains valid for 24 hours so you can deploy at your own pace. Update both, then drop the old one once your deploys are out.
  </Accordion>

  <Accordion title="Use the events log to spot regressions">
    Filter by `status=failed` or `status=dead` in the dashboard or API. Anything that lingers there is either an endpoint bug, a network blip, or a real automation issue worth digging into.
  </Accordion>
</AccordionGroup>

## API reference

| Method   | Path                              | Purpose                                                   |
| -------- | --------------------------------- | --------------------------------------------------------- |
| `POST`   | `/v1/webhooks`                    | Create a webhook (returns the signing secret once).       |
| `GET`    | `/v1/webhooks`                    | List webhooks for the project.                            |
| `GET`    | `/v1/webhooks/{id}`               | Fetch a single webhook config.                            |
| `PATCH`  | `/v1/webhooks/{id}`               | Update url, description, subscribed events, or `enabled`. |
| `POST`   | `/v1/webhooks/{id}/rotate-secret` | Rotate the signing secret. Old secret valid for 24 h.     |
| `DELETE` | `/v1/webhooks/{id}`               | Soft-delete a webhook.                                    |
| `POST`   | `/v1/webhooks/{id}/test`          | Fire a synthetic event of a given type.                   |
| `GET`    | `/v1/webhooks/{id}/events`        | Paginated delivery history (last 7 days).                 |

All routes are authenticated with the `anchor-api-key` header (or the dashboard session cookie) and scoped to the current project.

## Limits

* Up to 5 webhook endpoints per project.
* HTTPS only. The following hosts are rejected at create/update time:
  * Loopback / private / link-local IPs (`127.0.0.0/8`, `10.0.0.0/8`, `192.168.0.0/16`, `172.16.0.0/12`, `169.254.0.0/16`, IPv6 ULA `fc00::/7` and link-local `fe80::/10`).
  * Cloud metadata services (`169.254.169.254`, `metadata.google.internal`, `metadata.azure.com`).
  * Internal-only TLDs (`*.local`, `*.internal`).
  * Cloud control-plane endpoints — AWS STS (`sts.*.amazonaws.com`, including FIPS, GovCloud, and China regions), AWS IAM, EKS auth.
  * URLs with embedded basic-auth credentials.
* Maximum URL length: 2048 characters.
* Maximum payload size: 64 KB. Larger `output.value` is replaced with `"__truncated__"`; fetch the full result via the REST API.
* HTTP timeout: 10s per attempt.
* Maximum 6 delivery attempts (1 initial + 5 retries) over \~80 seconds.
* Delivery history retention: 7 days.
