Skip to content
developers
Developer home

VISWOO DOCUMENTATION

Your own server

Signed webhook events, signature verification examples (Node, .NET, PHP) and the install token.

When the built-in functions are not enough (connecting to an external system, your own logic), the plugin gets its own server. In the manifest, server and the server permission:

"permissions": ["settings", "data", "server"],
"server": {
"webhookUrl": "https://plugin.example.com/viswoo/webhook",
"events": ["installed", "settings.changed", "action"]
}

The address must be https; addresses pointing to internal networks are rejected. Without events every event except event is sent; list the frequent event explicitly to receive it.

The platform POSTs every event with a JSON body:

{ "id": "…", "type": "action", "code": "plugin-code", "tenantId": "…", "createdAt": "2026-11-01T10:00:00Z", "data": {} }
Event data Notes
installed { token, version, permissions } A new token every time the customer approves permissions; the old one stops working
settings.changed { settings } Server-only fields included
uninstalled {} The customer turned the plugin off (right away or at the end of the period). The install token no longer works; delete data according to your retention policy
event { source, screenId?, events: [{ name, count }] } Sent in batches as the screen or phone reports events with report. Only when listed explicitly in events
action { action, source, screenId?, payload } source: player or public. Synchronous: the JSON body returned within 5 s is the result of call

Events other than action are retried; deduplicate with id. For action, a non-2xx response makes call fail with plugin.server_error, no response within 5 s with plugin.server_timeout.

Tokens are per customer: store them by tenantId.

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

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

The secret is per plugin (whsec_…) and issued by Viswoo. Your server:

  1. Computes the signature over the raw body, before parsing it.
  2. Compares in constant time.
  3. Rejects a t more than 5 minutes in the past or future.
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)) return false;
if (!long.TryParse(t, out var time) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - time) > 300) return false;
var expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{t}.{rawBody}"));
byte[] given;
try { given = Convert.FromHexString(v1); } catch (FormatException) { return false; }
return CryptographicOperations.FixedTimeEquals(given, expected);
}
function viswoo_verify(string $secret, ?string $header, string $rawBody): bool {
parse_str(str_replace(',', '&', $header ?? ''), $parts);
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) return false;
if (abs(time() - (int) $parts['t']) > 300) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
$raw = file_get_contents('php://input');
if (!viswoo_verify(getenv('VISWOO_WEBHOOK_SECRET'), $_SERVER['HTTP_X_VISWOO_SIGNATURE'] ?? null, $raw)) {
http_response_code(401);
exit;
}

The server template contains a complete Node server: it verifies signatures, takes the token from installed, answers action and writes data through the platform API.

Terminal window
npx @viswoo/plugin-sdk init quote-of-the-day --template server
cd quote-of-the-day && npm install
npx viswoo-plugin dev . --webhook http://localhost:8787/webhook
npm run server

The simulator prints the signing secret and token on start; during development the template server reads the secret from the simulator’s state file.