Agent skill
Ampersand Webhooks Skill
Receive and verify Ampersand webhooks (delivered through Svix). Use when setting up an Ampersand webhook destination, debugging svix-signature verification, or handling notifications such as installation.created, connection.error, read.backfill.done and write.async.done, and read or subscribe result payloads.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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-signatureheader? - How do I handle
installation.created,connection.errororread.backfill.donenotifications? - 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:
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
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, where the window can be dropped or widened.
For complete handlers with tests, see examples/express/, examples/nextjs/, 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
# 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
npx hookdeck-cli listen 3000 ampersand --path /webhooks/ampersand
Reference Materials
- references/overview.md - Message kinds, events and payload shapes
- references/setup.md - Create a webhook destination and get the signing secret
- references/verification.md - Verification details, manual verifier, gotchas