Gareth Wilson Gareth Wilson

Guide to Wistia Webhooks: Features and Best Practices

Published


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

FeatureDetails
Signaturehex HMAC-SHA256
Signature headerx-wistia-signature
BodyJSON
Event routingBody field events[].type

Events and payloads

EventTriggered WhenCommon Use Cases
media.createdA media is createdTrack new uploads
media.processingProcessing is startedShow a "processing" state
media.readyProcessing is complete and all video and image assets are encodedPublish the video, notify the uploader
media.failedProcessing failedAlert and retry the upload
media.updatedA media is replaced or its title, description or thumbnail changesSync metadata (see previous_attributes)
media.transcript_updatedNew captions are generated (automated or human)Fetch the transcript for transcript.language_code
media.transcript_failedTranscript generation failedAlert; see transcript.reason
media.deletedA media is deletedRemove it from your catalogue
media.translation_createdA translation is created for a mediaStore the translated transcript
media.localization_createdA localization is created for a mediaPublish the localized version
media.localization_failedA localization failedAlert and retry
viewing_session.playA viewer plays a mediaEngagement analytics
viewing_session.percent_watchedA viewer reaches a watch-percentage milestoneProgress tracking, lead scoring
viewing_session.turnstile.convertedA viewer submits a Wistia formSend the lead to a CRM
viewing_session.call_to_action.convertedA viewer clicks a call to actionConversion tracking
viewing_session.annotation.convertedA viewer clicks an annotation linkConversion tracking
live_event.registrationSomeone registers for a live eventAdd 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 on
  • type, from the table above
  • generated_at, an ISO-8601 UTC timestamp to order by
  • metadata.account_id
  • payload, with objects that depend on the type: media and project for media events (plus previous_attributes, transcript or localization where relevant), visitor, viewing_session and percent_watched for viewing events, and live_event and registration for 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/wistia for local development)

Register Your Endpoint and Set the Secret Key

  1. Open Account Settings and go to Webhooks.
  2. Paste your endpoint into POST URL.
  3. Type a long random Secret Key and store the same value as WEBHOOK_SECRET.
  4. Tick the events you want (see overview.md).
  5. 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_key, payload_body). 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 events and dispatch each item by its own type.
  • 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_at before 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.