# Give new displays a program automatically

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.

> **Type:** guide
> **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**

## 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:**

   ```ts
   // auto-assign.ts: node auto-assign.ts
   import { 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.update
   const 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](/recipes/create-web-display), 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](/webhooks/verify-signature). |

## Related

- [Create a display and give it content](/recipes/create-web-display)
- [Post to Teams or Slack when a display goes offline](/recipes/offline-alert-chat)
- [Event types: content, programs, devices, apps](/webhooks/event-types-content-devices)
