# Ein Display anlegen und bespielen

Am Ende steht ein neues Display mit Namen und Standort in Ihrer Flotte, es hat schon ein Programm, und der Fernseher muss nur noch eine Adresse öffnen. So rollen Sie Displays nach Ihrer eigenen Liste aus, etwa nach den Filialen in einer Tabelle.

> **Typ:** anleitung
> **Für wen:** Entwicklerinnen und Entwickler, die Displays aus dem eigenen System einrichten
> **Voraussetzung:** ein Schlüssel `ak_…` der Klasse **Individuell** mit `fleet.web-display.manage`, `location.list` und `schedule.update`, und für Ihre Organisation die Fähigkeit **Displays ohne Agent**
> **Ort:** Kommandozeile

## Warum ein Web-Display

Ein Web-Display braucht kein eigenes Gerät. Die API legt es mit einem Aufruf an, und jemand öffnet seine Adresse im Browser des Fernsehers. Geräte, die einen sechsstelligen Code zeigen, werden dagegen mit `POST /v1/pairings/claim` gekoppelt. Dafür braucht es das Recht `fleet.player.pair`, und das kann kein API-Schlüssel tragen. Ein Gerät koppelt deshalb weiterhin eine Person, die im Portal angemeldet ist.

## Schritte

1. **Suchen Sie den Standort.** `GET /v1/locations` liefert alle Standorte Ihrer Organisation in einer Antwort, ohne Blättern:

   ```bash
   curl -sS "$SZENERA_API/v1/locations" \
     -H "Authorization: Bearer $SZENERA_API_KEY" \
     | jq -r '.locations[] | "\(.id)  \(.name)"'
   ```

   ```text
   loc_01KFB7QPW0LOCATION00000001  Filiale Nord
   ```

2. **Legen Sie das Display an.** Name und Standort genügen. Der Idempotenz-Schlüssel macht eine Wiederholung nach einem Verbindungsabbruch sicher, es entsteht also nie versehentlich ein zweites Display:

   ```bash
   curl -sS -X POST "$SZENERA_API/v1/web-displays" \
     -H "Authorization: Bearer $SZENERA_API_KEY" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: rollout-filiale-nord-theke" \
     -d '{ "name": "Filiale Nord Theke", "locationId": "loc_01KFB7QPW0LOCATION00000001" }'
   ```

   ```json
   {
     "playerId": "ply_01KFB7QPW0PLAYER0000000001",
     "token": "drc_01KFB7QPW0TOKEN00000000001",
     "connectUrl": "https://webplayer.szenera.com/?reconnect=drc_01KFB7QPW0TOKEN00000000001",
     "expiresAt": "2026-09-11T10:00:00.000Z"
   }
   ```

   Das Display gibt es sofort. Es belegt einen gebuchten Platz und steht als **Nicht verbunden** in der Flotte, bis ein Browser `connectUrl` öffnet.

3. **Weisen Sie das Programm zu.** Öffnen Sie das Programm im Portal: Die Adresse endet mit seiner Kennung `sch_…`. Eine Szene (`scene`, `sceneId`), eine Playlist (`playlist`, `playlistId`) oder ein einzelnes Medium (`media`, `assetId`) geht genauso:

   ```bash
   curl -sS -X PUT "$SZENERA_API/v1/players/ply_01KFB7QPW0PLAYER0000000001/program" \
     -H "Authorization: Bearer $SZENERA_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "program": { "kind": "schedule", "scheduleId": "sch_01KFB7QPW0SCHEDULE0000001" } }'
   ```

   Die Reihenfolge ist egal. Ein Display, das schon vor dem Anschließen ein Programm hat, zeigt es, sobald der Browser da ist.

4. **Geben Sie die Adresse weiter.** Schicken Sie `connectUrl` an die Person am Fernseher, zum Beispiel per E-Mail. Sie gilt einmal und läuft nach 24 Stunden ab. Wer sie im Browser des Fernsehers öffnet, bindet diesen Browser an das Display.

## Der ganze Rollout als Skript

Das Skript liest eine CSV-Datei mit Kopfzeile und den Spalten Name und Standort, legt je Zeile ein Display an, gibt allen dasselbe Programm und schreibt Name und Adresse jedes Displays aus:

```text
name,location
Filiale Nord Theke,Filiale Nord
Filiale Nord Schaufenster,Filiale Nord
Filiale Süd Theke,Filiale Süd
```

```ts
// rollout.ts: node rollout.ts displays.csv
import { readFile } from 'node:fs/promises';

const API = process.env.SZENERA_API;
const KEY = process.env.SZENERA_API_KEY;
const PROGRAM = process.env.SZENERA_PROGRAM_ID; // sch_…

async function call(method: string, path: string, body?: unknown, idempotencyKey?: string) {
  const headers: Record<string, string> = {
    Authorization: `Bearer ${KEY}`,
    'Content-Type': 'application/json',
  };
  if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
  const res = await fetch(`${API}${path}`, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);
  return res.json();
}

const { locations } = await call('GET', '/v1/locations');
const locationIds = new Map(locations.map((location) => [location.name, location.id]));

const rows = (await readFile(process.argv[2], 'utf8')).trim().split('\n').slice(1);
for (const row of rows) {
  const [name, locationName] = row.split(',').map((cell) => cell.trim());
  const locationId = locationIds.get(locationName);
  if (!locationId) throw new Error(`Unknown location: ${locationName}`);

  // Gleicher Name, gleicher Schlüssel: Ein zweiter Lauf binnen 24 Stunden legt nichts doppelt an
  const display = await call(
    'POST',
    '/v1/web-displays',
    { name, locationId },
    `rollout-${encodeURIComponent(name)}`,
  );
  await call('PUT', `/v1/players/${display.playerId}/program`, {
    program: { kind: 'schedule', scheduleId: PROGRAM },
  });
  console.log(`${name}\t${display.connectUrl}`);
}
```

Für viele gleiche Displays an einem Standort legt `POST /v1/web-displays/batch` bis zu 50 mit nummerierten Namen in einem Aufruf an, ganz oder gar nicht. Die Routen der Web-Displays tragen `x-stability: beta`, siehe [Web-Displays](/de/api/web-displays).

## Ergebnis prüfen

- Das Display steht mit Namen, Standort und dem Zustand **Nicht verbunden** in der Flotte.
- `GET /v1/players/{id}/program` nennt das zugewiesene Programm.
- Sobald der Browser des Fernsehers die Adresse geöffnet hat, wechselt der Zustand auf **Online**, und das Programm läuft.

## Wenn etwas nicht klappt

| Symptom | Bedeutung | Lösung |
|---|---|---|
| `402 Billing.LicensesExhausted` | Alle gebuchten Plätze sind belegt. Die Antwort nennt `booked` und `used`. | Plätze dazubuchen oder Displays löschen, die Sie nicht mehr brauchen. |
| `403 Billing.FeatureNotEntitled` | Die Fähigkeit **Displays ohne Agent** ist für Ihre Organisation abgeschaltet. | Beim Betreiber der Plattform freischalten lassen. |
| `403 Permission.Denied` | Dem Schlüssel fehlt eines der drei Rechte. | Einen Schlüssel der Klasse **Individuell** mit genau diesen Rechten erzeugen. |
| `404 Location.NotFound` | Die Standort-Kennung gehört nicht zu Ihrer Organisation. | Die Kennung aus `GET /v1/locations` nehmen. |
| `422 Schedule.ContentNotFound` | Programm, Szene, Playlist oder Medium gibt es nicht. | Die Kennung in der Portal-Adresse prüfen. |
| Die Adresse führt zu einem Kopplungscode | Die Adresse war schon benutzt oder ist abgelaufen. | Mit `POST /v1/web-displays/{id}/link` eine neue ausstellen und diese öffnen. |

## Weiterführendes

- [Web-Displays](/de/api/web-displays)
- [Neuen Displays automatisch ein Programm geben](/de/recipes/auto-assign-program)
- [Idempotenz von Anfang an](/de/getting-started/idempotency)
