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-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:

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": {...} }:

EventFires when
installation.created / installation.updated / installation.deletedA customer installs, changes or removes the integration
connection.created / connection.refreshed / connection.updated / connection.deletedAn OAuth connection is established, refreshed, updated or removed
connection.errorAuthentication or token refresh fails
read.backfill.done / read.backfill.errorA historical sync completes or fails
read.schedule.pausedA scheduled sync is paused (often auth issues)
read.triggered.done / read.triggered.errorAn on-demand read completes or fails
write.async.doneAn asynchronous write finishes
subscribe.create.success / subscribe.update.success / subscribe.delete.successA subscribe action is set up, updated or removed
subscribe.create.error / subscribe.update.error / subscribe.delete.errorA subscribe action fails to set up, update or tear down
destination.webhook.disabledYour webhook destination was disabled (e.g. after repeated failures)
destination.delivery.errorDelivery 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →