Guide to Wistia Webhooks: Features and Best Practices
Wistia is a video hosting and marketing platform for businesses, with a customisable player, lead capture forms, calls to action and live events. Its webhooks report two kinds of activity: what happens to your media (uploads, processing, transcripts, translations, deletions) and what viewers do with it (plays, watch progress, form submissions and clicks). Marketing teams use them to push video leads into a CRM, score engagement, and publish videos as soon as they're ready.
This guide covers configuring a Wistia webhook and its Secret Key, the batched events array, verifying X-Wistia-Signature, and handling at-least-once, unordered delivery with a 10-second timeout.
What are Wistia webhooks?
Wistia webhooks are configured under Account Settings > Webhooks. A webhook has a POST URL, a Secret Key that you choose and the event types to send. Wistia doesn't generate the key for you and has no other authentication on the request, so the signature is the only proof a delivery came from Wistia.
Deliveries are always JSON with a User-Agent of Wistia-Webhooks/{VERSION}. Wistia batches events that happen close together into one request, so the body holds an events array that may contain several events of different types.
Wistia webhook features
| Feature | Details |
|---|---|
| Signature | hex HMAC-SHA256 |
| Signature header | x-wistia-signature |
| Body | JSON |
| Event routing | Body field events[].type |
Events and payloads
| Event | Triggered When | Common Use Cases |
|---|---|---|
media.created | A media is created | Track new uploads |
media.processing | Processing is started | Show a "processing" state |
media.ready | Processing is complete and all video and image assets are encoded | Publish the video, notify the uploader |
media.failed | Processing failed | Alert and retry the upload |
media.updated | A media is replaced or its title, description or thumbnail changes | Sync metadata (see previous_attributes) |
media.transcript_updated | New captions are generated (automated or human) | Fetch the transcript for transcript.language_code |
media.transcript_failed | Transcript generation failed | Alert; see transcript.reason |
media.deleted | A media is deleted | Remove it from your catalogue |
media.translation_ | A translation is created for a media | Store the translated transcript |
media.localization_ | A localization is created for a media | Publish the localized version |
media.localization_ | A localization failed | Alert and retry |
viewing_session.play | A viewer plays a media | Engagement analytics |
viewing_ | A viewer reaches a watch-percentage milestone | Progress tracking, lead scoring |
viewing_ | A viewer submits a Wistia form | Send the lead to a CRM |
viewing_ | A viewer clicks a call to action | Conversion tracking |
viewing_ | A viewer clicks an annotation link | Conversion tracking |
live_event.registration | Someone registers for a live event | Add the registrant to your system |
viewing_session.play fires the first time someone presses play during a visit to a page, or when the video autoplays, so it counts visits that played rather than every press of the button. percent_watched fires once at each of four milestones. turnstile.converted is Wistia's name for a Wistia Forms submission and can include the viewer's name as well as their email.
Event Payload Structure
The body is { "hook": { "uuid": ... }, "events": [ ... ] }. hook.uuid identifies the message. Each item in events has:
uuid, the event's own ID and the key to deduplicate ontype, from the table abovegenerated_at, an ISO-8601 UTC timestamp to order bymetadata.account_idpayload, with objects that depend on the type:mediaandprojectfor media events (plusprevious_attributes,transcriptorlocalizationwhere relevant),visitor,viewing_sessionandpercent_watchedfor viewing events, andlive_eventandregistrationfor sign-ups
media.duration is in seconds. Several of the examples in Wistia's docs (media.created, media.processing, media.updated, media.transcript_updated and media.deleted) contain trailing commas and aren't valid JSON, so copy field names from them but take test fixtures from real deliveries.
Published payload example
This is the live_event.registration sample from the webhook samples library, taken from Wistia's documentation:
{
"hook": {
"uuid": "bb812395-8d23-4285-9278-9ca014209ae7"
},
"events": [
{
"uuid": "09e7b27100b3a000",
"type": "live_event.registration",
"payload": {
"live_event": {
"id": "abc123xyz",
"name": "Product Launch Webinar",
"url": "https://company.wistia.com/live/abc123xyz",
"starts_at": "2024-04-15T14:00:00Z"
},
"registration": {
"visitor_key": "v20150227_c651bc81-8ec8-445b-b8a0-878d19278d35",
"registered_at": "2024-03-31T13:59:22Z",
"fields": {
"first_name": "Lenny",
"last_name": "Lavigne",
"email": "lenny@wistia.com",
"company": "Wistia",
"job_title": "Video Producer",
"phone": "+1-555-123-4567",
"country": "United States"
}
},
"location": {
"country": "United States",
"region": "Massachusetts",
"city": "Cambridge"
},
"utm": {
"campaign": "spring_launch",
"medium": "email",
"source": "newsletter"
},
"referrer_url": "https://wistia.com/blog/product-update"
},
"metadata": {
"account_id": "8lq25o0p9c"
},
"generated_at": "2024-03-31T13:59:22Z"
}
]
}
Setting up Wistia webhooks
Prerequisites
- A Wistia account with access to Account Settings
- A public endpoint URL (use
npx hookdeck-cli listen 3000 wistia --path /webhooks/wistiafor local development)
Register Your Endpoint and Set the Secret Key
- Open Account Settings and go to Webhooks.
- Paste your endpoint into POST URL.
- Type a long random Secret Key and store the same value as
WEBHOOK_SECRET. - Tick the events you want (see overview.md).
- Click Submit.
Wistia's settings table says you "should be using a sha256" signature, which hints that older webhooks may have been set up differently. If verification fails on a long-standing webhook, re-save it with a new Secret Key.
Test a delivery
Start a local receiver and forward requests with the Hookdeck CLI:
npx hookdeck-cli listen 3000 wistia --path /webhooks/wistia
Point the webhook at the CLI URL with media.created, media.ready and viewing_session.play selected. Upload a short video, wait for it to process, then play it from an embed. You'll see how events are batched and what the media and visitor objects look like for your account.
Securing Wistia webhooks
X-Wistia-Signature is the hex HMAC-SHA256 of the raw POST body, keyed with your Secret Key. Wistia's Ruby sample is OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_. Verify the whole body once, then process each event in it. There's no timestamp in the signature, so rely on event uuid values to spot repeats. See the webhooks documentation.
Node.js verification
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-wistia-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler loops over the batch, skips any event uuid it has seen, and keeps going if one event fails so the rest of the batch isn't lost. It returns 503 if anything failed; since processed events are recorded, a resend only repeats the failed ones.
const express = require('express');
function createWistiaApp({ hasEvent, recordEvent, handlers }) {
const app = express();
app.post('/webhooks/wistia', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
let parsed;
try {
parsed = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
const { events } = parsed;
if (!Array.isArray(events) || events.some((event) => !event || typeof event !== 'object' || !event.uuid)) {
return res.sendStatus(400);
}
let failures = 0;
for (const event of events) {
try {
if (await hasEvent(event.uuid)) continue;
const handler = handlers[event.type];
if (handler) await handler(event.payload, event.generated_at, event.metadata);
else console.log('Unhandled Wistia event', event.type);
await recordEvent(event.uuid);
} catch {
failures += 1;
}
}
return failures ? res.sendStatus(503) : res.sendStatus(200);
});
return app;
}
Keep each handler fast. The whole batch has to finish within 10 seconds, so queue anything that calls another API.
With a Hookdeck Wistia source in front, Hookdeck verifies the hex HMAC of the body against your Secret Key and replies 200 straight away. A delivery with a bad signature is recorded as a failed verification and stops at Hookdeck. Hookdeck forwards the batch as Wistia sent it, so your handler still loops over events.
Turn video engagement into reliable data. Hookdeck Event Gateway verifies Wistia's signature, responds inside the 10-second timeout and keeps each batch for retry and replay to your CRM sync.
Wistia webhook limitations and pain points
No documented retry schedule
The Problem: You can't tell whether a failed delivery will be retried or for how long.
Why It Happens: Wistia documents at-least-once delivery and a 10-second timeout, and says a message might be resent after a timeout, but gives no retry schedule.
Workarounds:
- Respond quickly and process from a queue.
- Reconcile media status through Wistia's API after an outage.
How Hookdeck Can Help: Hookdeck accepts each delivery immediately and retries your service on a schedule you control.
Batches mix event types
The Problem: A single request can carry, say, a media.updated and several viewing_session.play events, which complicates routing.
Why It Happens: Wistia combines events that occur close together into one POST.
Workarounds:
- Always loop over
eventsand dispatch each item by its owntype. - Track success per event, not per request.
How Hookdeck Can Help: Hookdeck records the full batch, so you can inspect and replay exactly what Wistia sent. A transformation can reshape the body before it reaches your service.
Unordered and repeated events
The Problem: media.processing can arrive after media.ready, and the same event can arrive twice.
Why It Happens: Wistia doesn't guarantee order and may resend a message, for example after a timeout.
Workarounds:
- Deduplicate on event
uuid. - Compare
generated_atbefore overwriting a media's stored state.
How Hookdeck Can Help: Hookdeck keeps every delivery with its arrival time, which helps when you need to trace an out-of-order sequence.
Unreliable docs examples
The Problem: Fixtures copied from the docs fail to parse.
Why It Happens: Five of the media examples have trailing commas.
Workarounds:
- Build fixtures from real deliveries in a test account.
How Hookdeck Can Help: Real request bodies are stored in Hookdeck and can be copied straight into tests.
Best practices
Order media state by generated_at
Store the generated_at of the last event applied to each media and ignore older ones, so a late media.processing can't undo media.ready.
Use previous_attributes on updates
media.updated includes previous_attributes, which tells you what changed. Sync only those fields rather than rewriting the whole record.
Route leads from conversions
Send turnstile.converted and live_event.registration to your CRM, and use percent_watched milestones to score how engaged each lead is.
Protect viewer data
Conversion and registration events include names, emails and locations. Keep them out of logs and apply your usual retention rules.
Conclusion
Wistia's webhooks are well suited to both media pipelines and marketing automation, with per-event UUIDs, timestamps and a straightforward signature. The receiver has to handle batches of mixed events, out-of-order and repeated delivery, a 10-second limit, and an unpublished retry policy.
Hookdeck Event Gateway adds stored deliveries, retries and replay to Wistia's at-least-once model.
Get started with Hookdeck for free and handle Wistia webhooks reliably in minutes.