Webhooks · Docs — Quadraviz
Skip to content

Developers

Webhooks

TL;DR — Add an https endpoint under Settings → API & webhooks. We POST to it when a scene goes live, a trigger fires, or a value changes. Verify the signature before you trust it.

Why you'd want one

Polling GET /channels/:id/state works, and for a control surface refreshing its own buttons it's usually fine.

Webhooks are for the other direction — when something outside Quadraviz needs to react. Logging every graphic that went on air. Telling a tally system. Posting to a production channel when a scoreboard changes. Anything where you care the moment it happens rather than up to a second later.

Add an endpoint

  1. Open Settings → API & webhooks.
  2. Paste your endpoint URL. It has to be https and reachable from the internet.
  3. Tick the events you want, or tick nothing to get everything.
  4. Optionally limit it to one channel.
  5. Click Add webhook.

The signing secret appears next. Copy it — you need it to verify deliveries.

Tick nothing rather than ticking all four if you want everything, including event types we add later. A subscription with an explicit list keeps getting exactly that list.

Events

EventFires when
channel.playedPreview was taken to air.
channel.scene_loadedA scene was cued or put on air.
channel.trigger_firedA trigger ran.
channel.bind_updatedA value changed.

Every event fires whether the change came from the controller or from the API, so an integration sees what a human operator did as well as what a script did.

What we send

A POST with a JSON body:

json
{
  "id": "7c2f…",
  "type": "channel.played",
  "orgId": "org_7f3a…",
  "channelId": "chan_91b2…",
  "createdAt": "2026-08-23T18:04:11Z",
  "data": { "sceneId": "scn_88fa…" }
}

And these headers:

HeaderCarries
X-Quadraviz-EventThe event type, so you can route without parsing.
X-Quadraviz-DeliveryThe event id. Repeated across retries — deduplicate on it.
X-Quadraviz-TimestampUnix seconds. Part of the signed material.
X-Quadraviz-Signaturev1= followed by the HMAC in hex.

Answer 2xx once you have accepted the delivery. Anything else counts as a failure.

Verify the signature

Your endpoint is a public URL. Anyone who finds it can POST to it, so the signature is what separates a real delivery from someone pretending.

It is an HMAC-SHA256 over the timestamp, a full stop, and the raw body — using the signing secret from when you created the webhook.

js
import crypto from "node:crypto";

function verify(req, rawBody, secret) {
  const ts  = req.headers["x-quadraviz-timestamp"];
  const sig = req.headers["x-quadraviz-signature"];

  // Reject anything stale, or a captured delivery replays forever.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = "v1=" + crypto
    .createHmac("sha256", secret)
    .update(ts + ".")
    .update(rawBody)
    .digest("hex");

  // Constant-time — a plain === leaks the answer one byte at a time.
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Three things people get wrong here:

Check the timestamp. Without it, a delivery someone captured once stays valid forever.

Compare in constant time. A plain === returns faster the earlier it finds a difference, which is enough to guess a signature one byte at a time.

Retries, and when we stop

We try three times, seconds apart, then move on.

That's deliberately short. These are show events — by the time a long backoff would deliver "the scoreboard went live", the scoreboard has been live for ten minutes and gone again. A stale event is worse than a missing one.

After 20 failures in a row we switch the endpoint off and show the last error in settings. A successful delivery resets that count, so an endpoint that blips and recovers is never at risk.

If yours has been disabled, fix it and add it again.

Endpoints we refuse

Your URL has to be https and resolve to a public address.

We refuse loopback, private ranges, .local and .internal names, and cloud metadata addresses — and we check again at delivery time, not only when you save it.

That's not us being difficult. Your webhook URL is something we connect to on your behalf, so without those checks the form would be a way to make our servers reach into networks they have no business reaching, including our own.

If you're testing locally, use a tunnel that gives you a public https URL.

Next steps