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
- Open Settings → API & webhooks.
- Paste your endpoint URL. It has to be
httpsand reachable from the internet. - Tick the events you want, or tick nothing to get everything.
- Optionally limit it to one channel.
- 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
| Event | Fires when |
|---|---|
channel.played | Preview was taken to air. |
channel.scene_loaded | A scene was cued or put on air. |
channel.trigger_fired | A trigger ran. |
channel.bind_updated | A 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:
{
"id": "7c2f…",
"type": "channel.played",
"orgId": "org_7f3a…",
"channelId": "chan_91b2…",
"createdAt": "2026-08-23T18:04:11Z",
"data": { "sceneId": "scn_88fa…" }
}
And these headers:
| Header | Carries |
|---|---|
X-Quadraviz-Event | The event type, so you can route without parsing. |
X-Quadraviz-Delivery | The event id. Repeated across retries — deduplicate on it. |
X-Quadraviz-Timestamp | Unix seconds. Part of the signed material. |
X-Quadraviz-Signature | v1= 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.
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
- The Quadraviz API — Read state and drive channels
- API keys — Permissions and channel locks