Agent skill
SavvyCal Webhooks Skill
Receive and verify SavvyCal webhooks. Use when setting up SavvyCal webhook handlers, debugging x-savvycal-signature verification, or handling scheduling events like event.created, event.rescheduled, event.canceled, event.changed, or event.checkout.completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill savvycal-webhooks
When to Use This Skill
- How do I receive SavvyCal webhooks?
- How do I verify the SavvyCal
x-savvycal-signatureheader? - How do I handle
event.created,event.rescheduled, orevent.canceledevents? - Why is my SavvyCal webhook signature verification failing?
- Setting up a SavvyCal webhook handler for bookings, approvals, or paid-event checkout
Verification (core)
SavvyCal signs the raw request body with HMAC-SHA256, keyed with the webhook's secret, and sends the result in the x-savvycal-signature header. There is no timestamp and no SDK helper, so verify manually.
The header is sha256= followed by uppercase hex, and most HMAC libraries output lowercase. Strip the sha256= prefix and compare the hex case-insensitively.
const crypto = require('crypto');
function verifySavvyCalSignature(rawBody, header, secret) {
if (!header || !secret) return false;
const received = header.trim().replace(/^sha256=/i, '').toLowerCase();
if (!/^[0-9a-f]{64}$/.test(received)) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
}
Read the body as raw bytes whatever the content type (in Express, express.raw({ type: '*/*' })), and parse JSON only after verification passes.
For complete handlers with route wiring, event dispatch, and tests, see:
Common Event Types
The payload is JSON with a top-level type (the event name) and id (payload_...).
| Event | Triggered When |
|---|---|
event.created | Someone books a meeting (or an organizer approves a pending one) |
event.requested | Someone books a meeting that requires approval |
event.approved / event.declined | An organizer approves or declines a requested event |
event.rescheduled | An existing event is rescheduled |
event.changed | An event is updated in any way, with details of the change |
event.canceled | An event is canceled |
event.checkout.pending | A paid event is booked and checkout is still pending |
event.checkout.expired | A paid event's checkout expires unpaid |
event.checkout.completed | A paid event's checkout succeeds |
event.attendee.added / event.attendee.canceled / event.attendee.rescheduled | Attendee changes on an event or group event |
poll.response.created / poll.response.updated | A poll response is created or updated |
workflow.action.triggered | A workflow action fires |
For the full event reference, see references/overview.md and SavvyCal's webhook documentation.
Environment Variables
# Secret shown next to the webhook in Settings > Integrations
SAVVYCAL_WEBHOOK_SECRET=your_webhook_secret_here
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 savvycal --path /webhooks/savvycal
Reference Materials
- references/overview.md - What SavvyCal webhooks are, common events
- references/setup.md - Registering a webhook, getting the secret
- references/verification.md - Signature verification details and gotchas