Agent skill
Wistia Webhooks Skill
Receive and verify Wistia webhooks. Use when setting up Wistia webhook handlers, debugging X-Wistia-Signature verification, or handling events like media.ready, media.created, viewing_session.play, viewing_session.percent_watched, or live_event.registration.
Install this skill
npx skills add hookdeck/webhook-skills --skill wistia-webhooks
When to Use This Skill
- How do I receive Wistia webhooks?
- How do I verify the Wistia
X-Wistia-Signatureheader? - How do I know when a Wistia video is ready for playback (
media.ready)? - How do I handle Wistia viewing session events such as
viewing_session.play? - Why is my Wistia webhook signature verification failing?
- How do I handle batched Wistia deliveries and duplicate events?
Verification (core)
Every Wistia delivery carries an X-Wistia-Signature header: the lowercase hex HMAC-SHA256 of the raw request body, keyed with the Secret Key you entered when configuring the webhook. The header is the bare 64-character digest (no prefix), and nothing else (no timestamp or id) is signed. Wistia has no webhook SDK, so verify manually.
const crypto = require('crypto');
const HEX_SHA256 = /^[0-9a-fA-F]{64}$/;
function verifyWistiaSignature(rawBody /* Buffer */, signature, secret) {
if (!secret || typeof signature !== 'string' || !HEX_SHA256.test(signature)) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(signature.toLowerCase(), 'hex'); // validated above
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Use the raw body bytes (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()), use the secret's UTF-8 bytes as-is, and parse JSON only after the check. Respond 401 on a missing, malformed or mismatched signature.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Handling
The body is { "hook": { "uuid" }, "events": [ ... ] }. One POST can batch several events, so loop over events and dispatch on each event's type. Each event has uuid, type, generated_at and payload. Wistia delivers at least once (it may resend, for example after the 10 second timeout), so dedupe on each event's uuid and order by generated_at.
Common Event Types
| Event | Triggered When |
|---|---|
media.created | A media is created |
media.processing | Processing has started |
media.ready | Processing is complete and the media is ready for playback |
media.failed | Processing failed |
media.updated | A media is replaced or its title, description or thumbnail changes |
media.deleted | A media is deleted |
viewing_session.play | A viewer plays a media |
viewing_ | A viewer reaches a watch-percentage milestone |
viewing_ | A viewer submits a Wistia form |
live_event.registration | Someone registers for a live event |
For the full event list (transcripts, translations, localizations, call-to-action and annotation conversions), see references/overview.md and Wistia's webhook docs.
Environment Variables
# The Secret Key you entered for the webhook in Wistia (Account Settings > Webhooks)
WISTIA_WEBHOOK_SECRET=your_wistia_webhook_secret_key
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 wistia --path /webhooks/wistia
Reference Materials
- references/overview.md - Wistia webhook concepts, event list, payload shape
- references/setup.md - Configuring a webhook in Account Settings
- references/verification.md - Signature verification details and gotchas