Skip to content
Guide

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.

3 min readUpdated Oct 30, 2026As Markdown
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
On this page

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:

  1. Szenera sends the event fleet.player.status-changed with playerId, from, and to.
  2. The receiver checks the signature, ignores repeats, and reads the name of the display through the API.
  3. 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

  1. 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.
  2. 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);
    Terminal
    export WEBHOOK_SECRET="whsec_…" CHAT_WEBHOOK_URL="https://hooks.slack.com/services/…"node offline-alert.ts

    Put the receiver behind your https address, for example https://alerts.example.com/szenera. The seen set 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.

  3. Create the endpoint in the portal under Settings › For developers › Webhooks with Create endpoint. Enter the address under Endpoint URL, choose fleet.player.status-changed under Subscribed events, and click Create. Copy the secret from Signing secret — visible only now into WEBHOOK_SECRET and restart the receiver.

    The API route POST /v1/webhooks works 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.

  4. 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.