Post to Teams or Slack when a display goes offline
A small receiver turns the webhook for status changes into a message in your team chat.
- For
- Developers who look after displays for a team that lives in Slack or Microsoft Teams
- Prerequisite
- a package with webhooks (Premium and above), the Owner role for the endpoint, a key
ak_…of the class Read only for the display names, and a server with a public https address - Where
- your own server and Settings › For developers › Webhooks
By the end, a small receiver posts a message to your team chat when a display goes offline and again when it is back. Nobody has to keep the fleet open to notice that the TV in the shop window has gone dark.
How it works
Szenera can't post to a chat directly, because the chat expects its own message format and Szenera signs every delivery. A receiver of your own sits in between:
- Szenera sends the event
fleet.player.status-changedwithplayerId,from, andto. - The receiver checks the signature, ignores repeats, and reads the name of the display through the API.
- It posts a message to the incoming webhook of your chat.
A display counts as offline when it hasn't checked in for five minutes. Before that, it is stale for a short time. The receiver reports only the step to offline and the return to online, so a short wobble doesn't flood the chat.
Steps
-
Create the incoming webhook in your chat.
- Slack: create an app with Incoming Webhooks for the channel. The address begins with
https://hooks.slack.com/. - Microsoft Teams: in the channel, open Workflows and create a flow from a template that posts incoming webhook requests to the channel. The flow gives you an address that accepts Adaptive Cards.
- Slack: create an app with Incoming Webhooks for the channel. The address begins with
-
Save the receiver on your server and start it with the four values:
TypeScript// offline-alert.ts: node offline-alert.tsimport { createServer } from 'node:http';import { createHmac, timingSafeEqual } from 'node:crypto'; const SECRET = process.env.WEBHOOK_SECRET ?? ''; // whsec_…const API = process.env.SZENERA_API;const KEY = process.env.SZENERA_API_KEY; // class Read onlyconst CHAT = process.env.CHAT_WEBHOOK_URL ?? ''; // Slack or Teamsconst 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 displayName(playerId: string): Promise<string> { const res = await fetch(`${API}/v1/players/${playerId}`, { headers: { Authorization: `Bearer ${KEY}` }, }); return res.ok ? (await res.json()).player.name : playerId;} function chatMessage(text: string): unknown { if (CHAT.startsWith('https://hooks.slack.com/')) return { text }; return { type: 'message', attachments: [ { contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.4', body: [{ type: 'TextBlock', text, wrap: true }] }, }, ], };} 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) || req.headers['x-event-type'] !== 'fleet.player.status-changed') return; seen.add(deliveryId); const { playerId, from, to } = JSON.parse(raw); let text = ''; if (to === 'offline') text = `${await displayName(playerId)} is offline.`; if (to === 'online' && from === 'offline') text = `${await displayName(playerId)} is back online.`; if (!text) return; await fetch(CHAT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(chatMessage(text)), }); });}).listen(8080);Terminalexport WEBHOOK_SECRET="whsec_…" CHAT_WEBHOOK_URL="https://hooks.slack.com/services/…"node offline-alert.tsPut the receiver behind your https address, for example
https://alerts.example.com/szenera. Theseenset lives in memory. If the receiver restarts, a retry can post a second message, which is harmless here. For anything that must happen only once, store the IDs. -
Create the endpoint in the portal under Settings › For developers › Webhooks with Create endpoint. Enter the address under Endpoint URL, choose
fleet.player.status-changedunder Subscribed events, and click Create. Copy the secret from Signing secret — visible only now intoWEBHOOK_SECRETand restart the receiver.The API route
POST /v1/webhooksworks too, but only with the access token of an Owner sign-in. An API key can't carry the webhook permissions, see Create a webhook endpoint. -
Try it. Pull the network cable of a display, or close the browser tab of a web display. After about five minutes, the message arrives in the chat. Reconnect it, and the second message follows.
Check the result
- In the Delivery log of the endpoint, every delivery shows Delivered with
HTTP 204. - The chat shows the name of the display, not its ID. If the ID appears, the key can't read the display.
- A delivery repeated with Retry in the delivery log doesn't post a second message.
If something goes wrong
| Symptom | Meaning | Solution |
|---|---|---|
The delivery log shows HTTP 401 |
The signature doesn't match: wrong secret, or a proxy changes the body. | Copy the secret again, and make sure nothing parses the body before the check. |
| The delivery log shows Delivered, but the chat stays empty | The chat rejects the message. | Log the response of the chat. Slack answers invalid_payload for a wrong format, and a Teams workflow shows the failed run in its history. |
| The chat shows the ID instead of the name | GET /v1/players/{id} fails: the key is missing, expired, or lacks fleet.player.read. |
Use a key of the class Read only. |
| No delivery after five minutes | The endpoint isn't subscribed to the event, or the display was already offline. | Check Subscribed events, and see When no delivery arrives. |