# Ampersand Webhooks

Ampersand is a deep-integration platform for SaaS builders. A webhook destination receives two kinds of
messages: **notifications** (`{ "notificationType": "...", "data": {...} }`) and **read / subscribe
results** (`{ "action": "read" | "subscribe", "groupRef", "objectName", "result", "resultInfo", ... }`).
Ampersand uses Svix to deliver them, which is why the signature is in the `svix-signature` header.

## When to Use This Skill

* How do I receive Ampersand webhooks?
* How do I verify the Ampersand `svix-signature` header?
* How do I handle `installation.created`, `connection.error` or `read.backfill.done` notifications?
* How do I process read or subscribe results (inline or `resultInfo.downloadUrl`)?
* Why is my Ampersand webhook signature verification failing, or failing on retries?

## Verification (core)

Headers: `svix-id`, `svix-timestamp` (Unix seconds), `svix-signature` (one or more space-separated
`v1,<base64>` entries). Signed content is `{svix-id}.{svix-timestamp}.{raw body}`, HMAC-SHA256, key =
base64-decoded secret after the `whsec_` prefix, base64 output. The secret is the destination's
**Webhook Signing Secret**. Use the `svix` library (npm `svix`, pip `svix`) with the **raw body**:

```javascript
import { Webhook } from 'svix';

// rawBody: Buffer/string exactly as received (express.raw({ type: '*/*', limit: '350kb' }))
new Webhook(process.env.AMPERSAND_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': req.headers['svix-id'],
  'svix-timestamp': req.headers['svix-timestamp'],
  'svix-signature': req.headers['svix-signature'],
}); // throws on a bad signature or a timestamp more than 5 minutes off
const message = JSON.parse(rawBody.toString('utf8'));
// notificationType present -> notification, otherwise a read/subscribe result

```

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

Webhook(os.environ["AMPERSAND_WEBHOOK_SECRET"]).verify(raw_body, {
    "svix-id": headers["svix-id"],
    "svix-timestamp": headers["svix-timestamp"],
    "svix-signature": headers["svix-signature"],
})  # raises WebhookVerificationError; svix 2.x returns None, so json.loads(raw_body) afterwards

```

Reject missing headers or a bad signature with 400 (401 also works). Verify before parsing.

> **Retries and replays:** the svix library always enforces a fixed 5-minute timestamp window. Hookdeck
> retries and replays resend the original timestamp, so a retry after 5 minutes is rejected. If you
> receive through Hookdeck, use the manual verifier in
> [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ampersand-webhooks/references/verification.md), where the window can be dropped or widened.

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

## Common Event Types

Notifications arrive as `{ "notificationType": "<type>", "data": {...} }`:

| Event | Fires when |
| --- | --- |
| `installation.created` / `installation.updated` / `installation.deleted` | A customer installs, changes or removes the integration |
| `connection.created` / `connection.refreshed` / `connection.updated` / `connection.deleted` | An OAuth connection is established, refreshed, updated or removed |
| `connection.error` | Authentication or token refresh fails |
| `read.backfill.done` / `read.backfill.error` | A historical sync completes or fails |
| `read.schedule.paused` | A scheduled sync is paused (often auth issues) |
| `read.triggered.done` / `read.triggered.error` | An on-demand read completes or fails |
| `write.async.done` | An asynchronous write finishes |
| `subscribe.create.success` / `subscribe.update.success` / `subscribe.delete.success` | A subscribe action is set up, updated or removed |
| `subscribe.create.error` / `subscribe.update.error` / `subscribe.delete.error` | A subscribe action fails to set up, update or tear down |
| `destination.webhook.disabled` | Your webhook destination was disabled (e.g. after repeated failures) |
| `destination.delivery.error` | Delivery to a destination failed after all retries |

A body **without** `notificationType` is a read/subscribe result: dispatch on `action`, read `result`
when `resultInfo.type` is `inline`, or GET `resultInfo.downloadUrl` (expires in 15 minutes) when it is `url`.

## Environment Variables

```bash
# Webhook Signing Secret of the destination (starts with whsec_)
AMPERSAND_WEBHOOK_SECRET=whsec_xxxxx

```

Ampersand dashboard > Destinations > your destination > "Click here to view the secret".

## Local Development

```bash
npx hookdeck-cli listen 3000 ampersand --path /webhooks/ampersand

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ampersand-webhooks/references/overview.md) - Message kinds, events and payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ampersand-webhooks/references/setup.md) - Create a webhook destination and get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ampersand-webhooks/references/verification.md) - Verification details, manual verifier, gotchas