Skip to content
Guide

Give new displays a program automatically

Every new display gets the program of its location by itself, through a webhook and one API call.

3 min readUpdated Oct 30, 2026As Markdown
For
Developers who look after many locations or roll out displays in waves
Prerequisite
a package with webhooks (Premium and above), the Owner role for the endpoint, a key ak_… of the class Custom with fleet.player.read and schedule.update, and a server with a public https address
Where
your own server and Settings › For developers › Webhooks
On this page

By the end, every new display gets the program of its location by itself, a few seconds after it appears in the fleet. If a display moves to another location, it takes on that location's program. Nobody has to remember to assign content after a rollout.

How it works

Event When it comes What the receiver does
fleet.player.created A display was created, as a web display or by pairing a device Read its location and assign the program of that location.
fleet.player.updated with locationId in changedFields A display was moved to another location Assign the program of the new location. This replaces the previous assignment.

The receiver keeps a table of which location gets which program, plus a default for displays without a location.

Steps

  1. Write down the table of locations and programs as JSON. Take the location IDs from GET /v1/locations and the program IDs from the portal address:

    JSON
    {  "loc_01KFB7QPW0LOCATION00000001": "sch_01KWVAFRM0ZETPAN0000000001",  "loc_01KFB7QPW0LOCATION00000002": "sch_01KWVAFRM0ZETPAN0000000002",  "default": "sch_01KWVAFRM0ZETPAN0000000003"}
  2. Save the receiver:

    TypeScript
    // auto-assign.ts: node auto-assign.tsimport { createServer } from 'node:http';import { createHmac, timingSafeEqual } from 'node:crypto';import { readFileSync } from 'node:fs'; const SECRET = process.env.WEBHOOK_SECRET ?? ''; // whsec_…const API = process.env.SZENERA_API;const KEY = process.env.SZENERA_API_KEY; // fleet.player.read and schedule.updateconst PROGRAMS: Record<string, string> = JSON.parse(readFileSync('programs.json', 'utf8'));const seen = new Set<string>(); function isFromSignage(rawBody: string, header: string): boolean {  const expected = createHmac('sha256', SECRET).update(rawBody).digest('base64url');  const a = Buffer.from(expected);  const b = Buffer.from(header);  return a.length === b.length && timingSafeEqual(a, b);} async function call(method: string, path: string, body?: unknown) {  const res = await fetch(`${API}${path}`, {    method,    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },    body: body === undefined ? undefined : JSON.stringify(body),  });  if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);  return res.json();} async function assignProgramOfLocation(playerId: string): Promise<void> {  const { player } = await call('GET', `/v1/players/${playerId}`);  const scheduleId = PROGRAMS[player.locationId ?? ''] ?? PROGRAMS.default;  if (!scheduleId) return;  await call('PUT', `/v1/players/${playerId}/program`, {    program: { kind: 'schedule', scheduleId },  });  console.log(`${player.name}: ${scheduleId}`);} createServer((req, res) => {  const chunks: Buffer[] = [];  req.on('data', (chunk) => chunks.push(chunk));  req.on('end', async () => {    const raw = Buffer.concat(chunks).toString('utf8');    if (!isFromSignage(raw, String(req.headers['x-signature'] ?? ''))) {      res.writeHead(401).end();      return;    }    res.writeHead(204).end(); // Answer within 10 seconds, work afterwards     const deliveryId = String(req.headers['x-delivery-id']);    if (seen.has(deliveryId)) return;    seen.add(deliveryId);     const event = req.headers['x-event-type'];    const payload = JSON.parse(raw);    const moved = event === 'fleet.player.updated' && payload.changedFields?.includes('locationId');    if (event === 'fleet.player.created' || moved) {      await assignProgramOfLocation(payload.playerId).catch((error) => console.error(error));    }  });}).listen(8080);
  3. Create the endpoint in the portal under Settings › For developers › Webhooks with Create endpoint. Enter your https address, choose fleet.player.created and fleet.player.updated under Subscribed events, and click Create. Copy the secret into WEBHOOK_SECRET and start the receiver.

  4. Try it with a new web display, see Create a display and give it content, but without assigning a program. A few seconds later, GET /v1/players/{id}/program names the program of its location.

Assigning the same program twice is harmless, so a retried delivery does no damage even after a restart of the receiver.

Check the result

  • The receiver logs one line per new display with its name and program.
  • In the portal, the new display shows the program of its location.
  • In the Delivery log of the endpoint, every delivery shows Delivered with HTTP 204.

If something goes wrong

Symptom Meaning Solution
The receiver logs 403 The key lacks fleet.player.read or schedule.update. Create a key of the class Custom with both permissions.
New displays get the default program They have no location yet when they are created. Assign the location in the portal. The move triggers fleet.player.updated, and the receiver assigns the right program.
A manually chosen program was replaced The display was moved to another location. Leave fleet.player.updated out of the subscribed events if moves should keep the program.
The delivery log shows HTTP 401 The secret doesn't match. Renew it with Rotate secret and set the new one, see Verify the signature of a delivery.