Give new displays a program automatically
Every new display gets the program of its location by itself, through a webhook and one API call.
- 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 withfleet.player.readandschedule.update, and a server with a public https address - Where
- your own server and Settings › For developers › Webhooks
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
-
Write down the table of locations and programs as JSON. Take the location IDs from
GET /v1/locationsand the program IDs from the portal address:JSON{ "loc_01KFB7QPW0LOCATION00000001": "sch_01KWVAFRM0ZETPAN0000000001", "loc_01KFB7QPW0LOCATION00000002": "sch_01KWVAFRM0ZETPAN0000000002", "default": "sch_01KWVAFRM0ZETPAN0000000003"} -
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); -
Create the endpoint in the portal under Settings › For developers › Webhooks with Create endpoint. Enter your https address, choose
fleet.player.createdandfleet.player.updatedunder Subscribed events, and click Create. Copy the secret intoWEBHOOK_SECRETand start the receiver. -
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}/programnames 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. |