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-Signature header?
  • 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

EventTriggered When
media.createdA media is created
media.processingProcessing has started
media.readyProcessing is complete and the media is ready for playback
media.failedProcessing failed
media.updatedA media is replaced or its title, description or thumbnail changes
media.deletedA media is deleted
viewing_session.playA viewer plays a media
viewing_session.percent_watchedA viewer reaches a watch-percentage milestone
viewing_session.turnstile.convertedA viewer submits a Wistia form
live_event.registrationSomeone 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →