Skip to content
developers
Developer home

VISWOO DOCUMENTATION

Webhooks

Receive screen, media and approval events on your own server; signature verification, retries and delivery log.

A webhook is a signed POST request to your server when something happens in Viswoo. Addresses are managed in the panel under Add-ons → API access or through the API (webhooks:manage). At most 5 addresses per business; the address must be https, and requests are never sent to internal network addresses.

Event When data
screen.offline A screen hasn’t been seen for a while (same time as the panel’s offline email) { screenId, screenName, lastSeenAt }
screen.online An offline screen reconnected { screenId, screenName, lastSeenAt }
media.processed Uploaded media was processed and can be used { mediaId, name, type, status, failureCode }
media.failed Media could not be processed { mediaId, name, type, status, failureCode }
approval.requested An editor’s change awaits approval (Content approval) { changeId, kind, playlistId, title, requestedBy }
approval.decided An approval request was approved or rejected { changeId, kind, playlistId, title, status, decidedBy, note }
webhook.test “Send test” from the panel or the API { message }

Body:

{
"id": "0195…",
"event": "media.processed",
"createdAt": "2026-11-01T09:30:00Z",
"data": { "mediaId": "…", "name": "summer-sale", "type": "image", "status": "ready", "failureCode": null }
}

id belongs to the event: the same event goes to every address with the same id and doesn’t change on retries. Use id to drop duplicates.

Every request carries an X-Viswoo-Signature: t=<unix seconds>,v1=<hex> header:

v1 = HMAC-SHA256(secret, "<t>.<raw body>")

The secret is per address (whsec_…) and is shown only once, when the address is created or the secret rotated. Your server should compute the signature over the raw body before parsing it, compare in constant time and reject a t older than 5 minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(
String(header ?? '')
.split(',')
.map((part) => part.split('=')),
);
const t = Number(parts.t);
if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
using System.Security.Cryptography;
using System.Text;
static bool Verify(string secret, string? header, string rawBody)
{
var parts = (header ?? "").Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1]);
if (!parts.TryGetValue("t", out var t) || !parts.TryGetValue("v1", out var v1) || !long.TryParse(t, out var unix)
|| Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - unix) > 300)
{
return false;
}
var expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{t}.{rawBody}"));
return CryptographicOperations.FixedTimeEquals(expected, Convert.FromHexString(v1));
}
  • Answer 2xx within 10 seconds; queue the work and respond right away.
  • A non-2xx answer or a connection error is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 15 h (about 24 hours in total).
  • An address that fails 50 times in a row is paused and the account’s admins get an email. Fix the problem and turn it back on in the panel or with PUT /webhooks/{id} (enabled: true); events from the paused period aren’t resent.
  • Deliveries of the last 7 days are shown in the panel and by GET /webhooks/{id}/deliveries.
  • Rotate the secret with POST /webhooks/{id}/rotate-secret; the old secret stops working immediately.