# Gleichzeitige Änderungen: ETag und If-Match

Nach dieser Anleitung ändern Sie eine versionierte Ressource so, dass eine gleichzeitige Änderung eines anderen Clients nicht verloren geht: Sie lesen das Kennzeichen aus der Kopfzeile `ETag`, schicken es als `If-Match` zurück und lösen einen `409`-Konflikt sauber auf.

> **Typ:** anleitung
> **Für wen:** Wer Szenen, Playlisten, Programme, Rollen oder Displays über die API ändert.
> **Voraussetzung:** ein Schlüssel `ak_…` mit einem schreibenden Recht — im Beispiel `scene.update` (Klasse **Inhalte-Pflege**)
> **Ort:** Kommandozeile

## Wie die Versionsprüfung funktioniert

Fünf Ressourcen führen ein Feld `version`: Szenen, Playlisten, Programme (Zeitpläne), Rollen und Displays. Bei einer Änderung senden Sie den gelesenen Wert als Kopfzeile `If-Match`. Hat inzwischen jemand anderes geschrieben, antwortet die API mit `409 Conflict.OptimisticLock`, und Sie lesen neu.

**Die Version ist der ETag.** Sie steht zweimal in der Antwort: als Feld `version` im Rumpf und als Kopfzeile `ETag: "7"` — ein starkes Kennzeichen, kein `W/`. Beide Auftritte tragen denselben Wert; welchen Sie lesen, ist Ihre Wahl. Die Kopfzeile ist auch für Browser-Clients auf anderen Ursprüngen freigegeben.

`ETag` senden die Lese- und die Änderungsroute derselben Ressourcen: `GET` und `PATCH` auf `/v1/scenes/{id}`, `/v1/playlists/{id}`, `/v1/schedules/{id}`, `/v1/roles/{id}` und `/v1/players/{id}` sowie `POST /v1/scenes/{id}/versions/{vid}/revert`. Trägt eine Antwort ausnahmsweise keine Fassung, bleibt die Kopfzeile weg, statt eine Null zu behaupten.

## Schritte

1. Lesen Sie die Ressource und merken Sie sich die Fassung — aus der Kopfzeile oder aus dem Rumpf:

   ```bash
   curl -sS -D - -o /dev/null "$SZENERA_API/v1/scenes/scn_01KJPX8NH0SZENE00000000001" \
     -H "Authorization: Bearer $SZENERA_API_KEY" | grep -i '^etag:'
   ```

   ```
   ETag: "7"
   ```

2. Senden Sie die Änderung mit `If-Match` und genau diesem Wert:

   ```bash
   curl -sS -X PATCH "$SZENERA_API/v1/scenes/scn_01KJPX8NH0SZENE00000000001" \
     -H "Authorization: Bearer $SZENERA_API_KEY" \
     -H "Content-Type: application/json" \
     -H 'If-Match: "7"' \
     -d '{ "description": "Startbild im Schaufenster: Wortmarke, Wochenplan und Uhrzeit." }'
   ```

   Die Antwort ist die Szene mit der neuen Version (hier auf vier Felder gekürzt):

   ```json
   { "id": "scn_01KJPX8NH0SZENE00000000001", "name": "Notdienst & Wetter", "version": 8, "updatedAt": "2026-07-06T08:30:00.000Z" }
   ```

   Ein inhaltsgleicher Patch lässt die Version stehen.

3. Kommt `409` zurück, hat jemand zwischen Schritt 1 und 2 geschrieben:

   ```json
   {
     "code": "Conflict.OptimisticLock",
     "message": "Scene draft was modified concurrently",
     "traceId": "trc_…",
     "details": { "sceneId": "scn_01KJPX8NH0SZENE00000000001", "expectedVersion": 7 }
   }
   ```

   Lesen Sie die Ressource neu (Schritt 1), wenden Sie Ihre Änderung auf den frischen Stand an und senden Sie erneut mit der neuen Version. Wiederholen Sie den Patch nicht blind mit der alten Version — das endet wieder in `409`.

## Optionen

`If-Match` nimmt **drei Schreibweisen desselben Werts** an:

| Kopfzeile | Wirkung |
|---|---|
| `If-Match: "7"` | die Regelform: das Kennzeichen, wie die Antwort es gesendet hat |
| `If-Match: W/"7"` | die schwache Form, ebenfalls angenommen |
| `If-Match: 7` | die nackte Zahl aus dem Rumpf — der Bestand, damit kein laufender Client bricht |
| `If-Match: 0` oder kein Zahlenwert | `422 Validation.Failed` („If-Match must be a positive resource version“) |
| kein `If-Match` | auf Routen, die die Kopfzeile erzwingen: `422 Validation.Failed` („If-Match header required for this operation“) |
| Displays | fehlende Kopfzeile endet in `409 Conflict.OptimisticLock` (Versionsvergleich gegen 0), nie in `422` |

Fünf Operationen **erzwingen** die Kopfzeile und führen sie in der Referenz als Pflicht-Parameter: `PATCH /v1/scenes/{id}`, `POST /v1/scenes/{id}/versions/{vid}/revert`, `PATCH /v1/playlists/{id}`, `PATCH /v1/schedules/{id}` und `PATCH /v1/roles/{id}`. `PATCH /v1/players/{id}` nimmt sie an, verlangt sie aber nicht: Ohne Kopfzeile prüft die Route gegen Fassung 0 und läuft damit in denselben Konflikt wie eine veraltete Angabe. Bei Programmen liegt `version` unter `schedule.version`, bei Rollen unter `role.version`, bei Displays unter `player.version`.

Was `details` bei einem Konflikt trägt, hängt von der Ressource ab: Szenen nennen `sceneId` und `expectedVersion`; Rollen `resource`, `resourceId`, `expectedVersion` und `actualVersion`; Displays `playerId`, `expectedVersion` und `actualVersion`; Playlisten und Programme `entity`, `id` und `expectedVersion`.

**Kommandozeile.** `szenera scene update <id> --version=7 …` sendet die Version als `If-Match`; ohne `--version` liest der Befehl sie vorher selbst. `--retry` liest bei einem Konflikt einmal neu und wiederholt. Die Spalte **Fassung** in `szenera scene list` ist derselbe Wert. Details unter [Befehle für Ihre Organisation](/de/cli/commands-organization).

## Ergebnis prüfen

- Die Antwort auf den Patch ist `200` mit der Ressource, und `version` ist um eins gestiegen (oder unverändert, wenn der Patch nichts geändert hat).
- Ein zweiter Patch mit dem alten `If-Match` antwortet `409 Conflict.OptimisticLock`.

## Wenn etwas nicht klappt

| Symptom | Bedeutung | Lösung |
|---|---|---|
| `409 Conflict.OptimisticLock` | Ein anderer Client hat zwischen Lesen und Schreiben geändert. | Neu lesen, Änderung erneut anwenden, mit neuer Version senden. |
| `422 Validation.Failed` mit „If-Match header required for this operation“ | Eine der fünf Routen oben erzwingt die Kopfzeile. | `If-Match` mit dem gelesenen Kennzeichen mitsenden. |
| `422 Validation.Failed` mit „If-Match must be a positive resource version“ | Der Wert ist keine Zahl ab 1 — etwa ein selbst gebildetes Kennzeichen aus mehreren Teilen. | Das Kennzeichen aus der Antwort senden, `"7"`, `W/"7"` oder `7`. |
| `409 Conflict.OptimisticLock` bei einem Display, obwohl niemand geschrieben hat | Die Kopfzeile fehlte; die Route vergleicht dann gegen Fassung 0. | `If-Match` mit dem gelesenen Kennzeichen mitsenden. |
| Sie erwarten `412` | Es gibt keinen `412` — der Konflikt ist immer `409`. | Auf `409` und den Code `Conflict.OptimisticLock` schalten. |
| Kein `ETag`-Kopf in der Antwort | Die Route ist nicht versioniert, oder die Antwort trug ausnahmsweise keine Fassung. | `version` aus dem Rumpf lesen; fehlt auch die, kennt die Ressource keine Fassung. |

## Weiterführendes

- [Idempotenz von Anfang an](/de/getting-started/idempotency)
- [Das Fehlerformat](/de/api/error-format)
- [Befehle für Ihre Organisation](/de/cli/commands-organization)
