# Concurrent changes: ETag and If-Match

This guide shows you how to change a versioned resource so that a concurrent change by another client is not lost: You read the tag from the `ETag` header, send it back as `If-Match`, and resolve a `409` conflict cleanly.

> **Type:** guide
> **For:** Anyone who changes scenes, playlists, programs, roles, or displays through the API.
> **Prerequisite:** an `ak_…` key with a write permission, in the example `scene.update` (class **Content authoring**)
> **Where:** command line

## How the version check works

Five resources carry a `version` field: scenes, playlists, programs (schedules), roles, and displays. When you make a change, you send the value you read as the `If-Match` header. If someone else has written in the meantime, the API answers with `409 Conflict.OptimisticLock`, and you read again.

**The version is the ETag.** It appears twice in the response: as the field `version` in the body and as the header `ETag: "7"`, a strong tag, not a `W/` one. Both carry the same value, and which one you read is your choice. The header is also exposed to browser clients on other origins.

The read route and the change route of the same resources send `ETag`: `GET` and `PATCH` on `/v1/scenes/{id}`, `/v1/playlists/{id}`, `/v1/schedules/{id}`, `/v1/roles/{id}`, and `/v1/players/{id}`, as well as `POST /v1/scenes/{id}/versions/{vid}/revert`. If a response carries no version in an exceptional case, the header is omitted instead of claiming a zero.

## Steps

1. Read the resource and note the version, from the header or from the body:

   ```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. Send the change with `If-Match` and exactly this value:

   ```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." }'
   ```

   The response is the scene with the new version (shortened here to four fields):

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

   A patch with identical content leaves the version as it is.

3. If `409` comes back, someone wrote between steps 1 and 2:

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

   Read the resource again (step 1), apply your change to the fresh state, and send again with the new version. Do not repeat the patch blindly with the old version, because that ends in `409` again.

## Options

`If-Match` accepts **three spellings of the same value**:

| Header | Effect |
|---|---|
| `If-Match: "7"` | the standard form: the tag exactly as the response sent it |
| `If-Match: W/"7"` | the weak form, also accepted |
| `If-Match: 7` | the bare number from the body, kept as is so that no running client breaks |
| `If-Match: 0` or a non-numeric value | `422 Validation.Failed` (“If-Match must be a positive resource version”) |
| no `If-Match` | on routes that enforce the header: `422 Validation.Failed` (“If-Match header required for this operation”) |
| Displays | a missing header ends in `409 Conflict.OptimisticLock` (version comparison against 0), never in `422` |

Five operations **enforce** the header and list it as a required parameter in the reference: `PATCH /v1/scenes/{id}`, `POST /v1/scenes/{id}/versions/{vid}/revert`, `PATCH /v1/playlists/{id}`, `PATCH /v1/schedules/{id}`, and `PATCH /v1/roles/{id}`. `PATCH /v1/players/{id}` accepts it but does not require it: Without the header, the route checks against version 0 and so runs into the same conflict as a stale value. For programs, `version` is located under `schedule.version`, for roles under `role.version`, and for displays under `player.version`.

What `details` carries for a conflict depends on the resource. Scenes name `sceneId` and `expectedVersion`. Roles name `resource`, `resourceId`, `expectedVersion`, and `actualVersion`. Displays name `playerId`, `expectedVersion`, and `actualVersion`. Playlists and programs name `entity`, `id`, and `expectedVersion`.

**Command line.** `szenera scene update <id> --version=7 …` sends the version as `If-Match`. Without `--version`, the command reads it itself beforehand. `--retry` reads again once on a conflict and repeats. The **Fassung** (version) column in `szenera scene list` is the same value. Details are described under [Commands for your organization](/cli/commands-organization).

## Check the result

- The response to the patch is `200` with the resource, and `version` has increased by one (or is unchanged if the patch changed nothing).
- A second patch with the old `If-Match` answers `409 Conflict.OptimisticLock`.

## If something goes wrong

| Symptom | Meaning | Solution |
|---|---|---|
| `409 Conflict.OptimisticLock` | Another client made a change between your read and your write. | Read again, apply the change again, and send with the new version. |
| `422 Validation.Failed` with “If-Match header required for this operation” | One of the five routes above enforces the header. | Send `If-Match` with the tag you read. |
| `422 Validation.Failed` with “If-Match must be a positive resource version” | The value is not a number from 1 upward, for example a self-built tag made of several parts. | Send the tag from the response: `"7"`, `W/"7"`, or `7`. |
| `409 Conflict.OptimisticLock` on a display although nobody wrote | The header was missing, so the route compares against version 0. | Send `If-Match` with the tag you read. |
| You expect `412` | There is no `412`. The conflict is always `409`. | Branch on `409` and the code `Conflict.OptimisticLock`. |
| No `ETag` header in the response | The route is not versioned, or the response carried no version in an exceptional case. | Read `version` from the body. If that is missing too, the resource has no version. |

## Related

- [Idempotency from the start](/getting-started/idempotency)
- [The error format](/api/error-format)
- [Commands for your organization](/cli/commands-organization)
