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

# Webhooks

> Test webhook delivery and inspect delivery logs. Signed callbacks for async crawl completion.

# Webhooks

Webhooks deliver **signed** callbacks to your server. They're used for asynchronous
`crawl.completed` notifications and can be tested manually before going live.

<Info>
  **Authentication:** API key required.
</Info>

## Signature verification

Every webhook is signed with **HMAC-SHA256**. The signature is computed over the
**canonical JSON** of the payload (stable key order, compact separators) and sent in
two places:

1. The `X-BaaS-Signature` header — `sha256=<hex>`
2. The `signature` field in the JSON body

```javascript Node.js — verify a signature theme={null}
import crypto from "crypto";

function canonicalize(payload) {
  return JSON.stringify(payload, Object.keys(payload).sort());
}

function verify(body, secret, signature) {
  const { signature: _sig, ...payload } = body;      // strip the signature field
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(canonicalize(payload))
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
```

```python Python — verify a signature theme={null}
import hmac, hashlib

def verify(body: dict, secret: str, signature: str) -> bool:
    payload = {k: v for k, v in body.items() if k != "signature"}
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    expected = "sha256=" + hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

***

## Test a webhook

`POST /v1/webhooks/test` sends a **single immediate** signed webhook to your URL (no
retry) and records the attempt in your delivery logs.

```
POST https://api.tazpal.com/v1/webhooks/test
```

### Body

| Field    | Type   | Required | Default        | Description                |
| -------- | ------ | -------- | -------------- | -------------------------- |
| `url`    | string | ✅        | —              | Callback URL to deliver to |
| `secret` | string | ✅        | —              | HMAC-SHA256 signing secret |
| `event`  | string | no       | `test.webhook` | Event name (≤ 50 chars)    |
| `data`   | object | no       | `{}`           | Arbitrary payload data     |

```bash theme={null}
curl -X POST https://api.tazpal.com/v1/webhooks/test \
  -H "Authorization: Bearer baas_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example.com/hooks/baas",
    "secret": "your_signing_secret",
    "event": "test.webhook",
    "data": {"hello": "world"}
  }'
```

```json Response — 200 OK theme={null}
{
  "message": "Webhook delivered",
  "delivery": {
    "delivery_id": "3f4c...",
    "event": "test.webhook",
    "callback_url": "https://yourapp.example.com/hooks/baas",
    "status": "delivered",
    "attempts": 1,
    "response_status": 200
  }
}
```

If delivery fails, the endpoint returns `502` with `detail.error: webhook_delivery_failed`.

***

## View delivery logs

`GET /v1/webhooks/logs` lists your delivery records, newest first.

```
GET https://api.tazpal.com/v1/webhooks/logs?limit=100&offset=0
```

| Query    | Default | Range  |
| -------- | ------- | ------ |
| `limit`  | `100`   | 1–1000 |
| `offset` | `0`     | ≥ 0    |

```bash theme={null}
curl "https://api.tazpal.com/v1/webhooks/logs?limit=20" \
  -H "Authorization: Bearer baas_live_YOUR_KEY"
```

```json Response theme={null}
{
  "count": 20,
  "deliveries": [
    {
      "delivery_id": "3f4c...",
      "event": "crawl.completed",
      "callback_url": "https://yourapp.example.com/hooks/baas",
      "status": "delivered",
      "attempts": 1,
      "max_attempts": 3,
      "last_attempt_at": "2026-08-28T14:31:00Z",
      "response_status": 200,
      "response_body": null,
      "error": null,
      "created_at": "2026-08-28T14:31:00Z"
    }
  ]
}
```

***

## Crawl completion callback

When a crawl job with a `callback_url` completes, X1-BaaS dispatches a signed
`crawl.completed` webhook:

```json Example payload theme={null}
{
  "event": "crawl.completed",
  "job_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "timestamp": "2026-08-28T14:31:00Z",
  "data": {
    "status": "completed",
    "pages_crawled": 300,
    "pages_failed": 3
  },
  "signature": "sha256=..."
}
```

## Retry behavior

Crawl-completion webhooks retry with **exponential backoff**:

| Attempt | Timing    |
| ------- | --------- |
| 1       | immediate |
| 2       | +10s      |
| 3       | +30s      |
| 4       | +60s      |

After the retry budget (3 retries after the initial attempt) the delivery is marked
`failed`.

<Warning>
  Always verify the `X-BaaS-Signature` header with a constant-time comparison before
  trusting a webhook payload.
</Warning>
