# Airweave Webhooks

[Airweave](https://airweave.ai) is an open-source context-retrieval layer that syncs apps into
searchable collections for agents. Its webhooks (beta) report sync-job, source-connection and
collection lifecycle events. They are delivered through Svix, which
implements Standard Webhooks. Every delivery is an HTTP `POST` with a JSON body and three
signature headers: `svix-id`, `svix-timestamp`, and `svix-signature`. The body is flat: the event
name is the top-level `event_type`, followed by the ids for that domain and a `timestamp`.

## When to Use This Skill

* How do I receive Airweave webhooks?
* How do I verify Airweave webhook signatures (`svix-signature`)?
* How do I handle `sync.completed`, `sync.failed` or `sync.cancelled` events?
* How do I react to `source_connection.auth_completed` or `collection.deleted`?
* Why is my Airweave webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256 using
the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `svix-*` headers, and when `svix-id` is absent fall back to the `webhook-*`
headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.AIRWEAVE_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["AIRWEAVE_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/airweave-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/airweave-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/airweave-webhooks/examples/fastapi/).

The signing secret is per subscription and starts with `whsec_`. Fetch it with
`GET /webhooks/subscriptions/{id}?include_secret=true`: the response includes a `secret` field in
the format `whsec_...`. Keep the full value in `AIRWEAVE_WEBHOOK_SECRET`. Handlers fail closed:
with the variable unset they return 500 instead of accepting anything.

> If you set a custom secret, paste the whsec_ secret Airweave returns for the subscription, not
> the value you sent.

That is Hookdeck's own configuration alert for this source. A custom `secret` (minimum 24
characters) can be supplied when you create the subscription, but what a verifier needs is the
returned `whsec_...` value.

Airweave's own manual-verification snippets split `svix-signature` on `,` and take the last
space-separated token, which only works for a single signature. The `svix` library and the manual
verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/verification.md) handle several
space-separated `v1,<base64>` entries correctly.

### Hookdeck retries and replays hit the 5-minute window

The `svix` library always enforces a fixed 5-minute timestamp window, and it can't be configured.
Hookdeck retries and replays resend the **original** `svix-timestamp`, so the library rejects any
retry or replay that happens more than 5 minutes after the original delivery. If you verify
behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/verification.md), where the window can be dropped or
widened. The body has no event id, so deduplicate on the `svix-id` header instead of relying on the timestamp.

## Common Event Types

11 events are documented (the list may not be exhaustive). The name is `event_type` in the JSON
body. The examples handle all of these:

| Event | Triggered When |
| --- | --- |
| `sync.pending` | A sync job is created and queued |
| `sync.running` | The sync job starts processing |
| `sync.completed` | The sync job finishes successfully (adds `entities_inserted`, `entities_updated`, `entities_deleted`, `entities_skipped`, `chunks_written`) |
| `sync.failed` | The sync job hit an error (`error` has the details) |
| `sync.cancelled` | The sync job was cancelled manually |
| `source_connection.created` | A source connection record is created (check `is_authenticated`) |
| `source_connection.auth_completed` | The OAuth flow completed and the connection is ready |
| `source_connection.deleted` | A source connection and its data were removed |
| `collection.created` | A collection was created |
| `collection.updated` | Collection properties changed |
| `collection.deleted` | A collection and its data were removed |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/overview.md) for payload fields, retries and delivery
rules.

## Environment Variables

```bash
# Signing secret of the webhook subscription (Svix format, starts with whsec_)
AIRWEAVE_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 airweave --path /webhooks/airweave

```

Create a subscription with `POST https://api.airweave.ai/webhooks/subscriptions` (Bearer token,
admin or owner role) using the tunnel URL and the event types you want, then fetch the secret with
`?include_secret=true`. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/overview.md) - Airweave webhook concepts, events, payloads, delivery rules
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/setup.md) - Create a subscription, get the secret, manage and recover
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/airweave-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window