# Locations

A location is the place where displays are installed: name, address, time zone, optionally the position on the map, the federal state, the opening hours, and the venue type. This page lists the fields, the routes, the assignment of a display, and the other places where the location appears.

> **Type:** reference
> **For:** Anyone who mirrors a branch structure from their own system or groups analytics by location.
> **Prerequisite:** an `ak_…` key with the `location.*` permissions, which are key-capable

## What a location is

A folder above displays, not a second tenant. It carries no permissions, no licenses, and no inventory of its own. A display points to it with `locationId`, and there is no tighter binding than that. Tags and video walls remain what they are: The location does not replace them, it answers a different question.

| Field | Rule |
|---|---|
| `id` | `loc_` and 26 characters |
| `name` | 1 to 120 characters, unique per organization |
| `address` | four required fields as soon as an address exists at all: `street`, `postalCode`, `city`, `country` (country code per ISO 3166-1 alpha-2, uppercase). `null` means “none on file” |
| `timeZone` | required, a zone from the IANA database (`Europe/Berlin`) |
| `coordinates` | `lat` and `lng` or `null`. Set by a person, without an accuracy value |
| `region` | the federal state as an identifier per ISO 3166-2 (`DE-HH`, currently the sixteen German federal states) or `null`. Determines the statutory holidays |
| `openingHours` | `{ weekly, specialDays }`, or `null` as long as no hours are maintained, see [Opening hours](#opening-hours) |
| `note` | free text up to 500 characters for the people on site |
| `venueType` | the venue type (`pharmacy`, `workshop`, `bakery`, `beverage_store`, `grocery`, `kiosk`, `fitness_studio`, `medical_practice`, `retail`, `other`) or `null`. Ad bookings are targeted by it, and analytics groups by it, and likewise by the federal state |
| `version` | the version for `If-Match`, see [ETag and If-Match](/api/etag-and-if-match) |

The time zone belongs to the location, not to the display: Two displays in the same room never have two different ones.

## The routes

| Route | Permission | Note |
|---|---|---|
| `GET /v1/locations` | `location.list` | complete, without paging. Each row gives the number of displays at the location in `playerCount` |
| `GET /v1/locations/{id}` | `location.read` | one location with all details |
| `POST /v1/locations` | `location.create` | `201`. Needs the **Locations** capability of the organization |
| `PATCH /v1/locations/{id}` | `location.update` | partial change. `address: null`, `coordinates: null`, `region: null`, `openingHours: null`, and `venueType: null` remove the value, and an omitted field stays as it is. `openingHours` replaces the previous value as a whole |
| `DELETE /v1/locations/{id}` | `location.delete` | only an empty location |

```bash
curl -sS -X POST "$SZENERA_API/v1/locations" \
  -H "Authorization: Bearer $SZENERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Filiale Nord",
        "timeZone": "Europe/Berlin",
        "address": { "street": "Nordstraße 12", "postalCode": "30159", "city": "Hannover", "country": "DE" }
      }'
```

## Assign a display

The assignment does not go through the location route but through the display: A location is a master data field of the display, like its name.

```bash
curl -sS -X PATCH "$SZENERA_API/v1/players/ply_01KFB7QPW0PLAYER0000000001" \
  -H "Authorization: Bearer $SZENERA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{ "locationId": "loc_01KFB7QPW0LOCATION00000001" }'
```

`"locationId": null` detaches the display again. A location never takes displays down with it: An occupied location cannot be deleted, so the displays are moved or detached first. When a display gets a different location or none at all, it rebuilds its snapshot immediately: Opening hours, pharmacies on duty, weather, and time zone follow the new location.

## Opening hours

`openingHours` carries the weekly hours and the special days. The times are wall-clock times in the time zone of the location.

| Field | Rule |
|---|---|
| `weekly.mon` … `weekly.sun` | per day up to six windows `{ "opens": "08:00", "closes": "13:00" }`, ascending and without overlap, empty means closed. If `closes` is not after `opens`, the window ends on the next day (`22:00` to `02:00`), and equal times mean 24 hours (`00:00` to `00:00` is open around the clock). A window across midnight is the last one of the day and does not extend into the first window of the next day. A missing day counts as closed |
| `specialDays[]` | up to 100 entries `{ startDate, endDate, label, closed, windows }`: a day or a period of up to 92 days (`endDate` inclusive), a label of up to 60 characters, closed without windows or open with at least one. Special days do not overlap |

What applies on a given day follows a fixed order of precedence: a special day, otherwise a statutory holiday of the federal state (closed), otherwise the weekly hours. The platform does not store the holidays. It calculates them per federal state from the date of Easter and the state holidays. Holidays of individual municipalities are entered by the organization as a special day.

```bash
curl -sS -X PATCH "$SZENERA_API/v1/locations/loc_01KFB7QPW0LOCATION00000001" \
  -H "Authorization: Bearer $SZENERA_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "3"' \
  -d '{
    "region": "DE-HH",
    "openingHours": {
      "weekly": {
        "mon": [{ "opens": "08:30", "closes": "13:00" }, { "opens": "14:00", "closes": "18:30" }],
        "sat": [{ "opens": "09:00", "closes": "14:00" }]
      },
      "specialDays": [
        { "startDate": "2026-12-24", "endDate": "2026-12-24", "label": "Heiligabend",
          "closed": false, "windows": [{ "opens": "08:30", "closes": "12:00" }] }
      ]
    }
  }'
```

If the weekly hours, special days, federal state, or time zone change, the event `fleet.location.opening-hours-changed` is raised in addition to `fleet.location.updated`. All displays of the organization then rebuild their snapshot, because a widget can show the hours of every location, not only its own. A display whose snapshot stays the same receives nothing, and the others show the new times after a few seconds. The same applies when a location is deleted.

## Where else the location appears

| Place | How |
|---|---|
| Display list | `GET /v1/players?locationId=loc_…` filters to one location |
| Playback analytics | `location` is a grouping axis. Grouping uses the **current** assignment, and a display without a location falls into a row of its own |
| Command line | the `location` group, plus `player assign-location` and `player unassign-location`, see [Commands for your organization](/cli/commands-organization) |
| Assistant | the tools `list_locations`, `get_location`, `assign_player_location`, `create_location`, and `set_location_opening_hours` of the [MCP service](/api/mcp-service) |
| Apps | the location of the display in the resolver context, with `location-hours` including opening hours, see [Data access, permissions, and terms](/apps-sdk/data-access) |

## If something goes wrong

| Symptom | Meaning | Solution |
|---|---|---|
| `409 Location.HasPlayers` | Displays are still attached to the location. The response gives the number. | Move or detach the displays, then delete. |
| `409 Location.NameAlreadyExists` | The name already exists in this organization. | Choose another one: Two rows named “Filiale Nord” would be a trap in every selection. |
| `404 Location.NotFound` | The identifier does not belong to any location of this organization. | Take the identifier from `GET /v1/locations`. |
| `422 Validation.Failed` on `timeZone` | The value is not a zone of the IANA database. | `Europe/Berlin` instead of `MEZ` or `+01:00`. |
| `403 Billing.FeatureNotEntitled` when creating | The **Locations** capability is switched off for this organization. | Have the platform operator enable it. |
| `409 Conflict.OptimisticLock` when changing | Someone else made a change between your read and your write. | Read again and send with the new version. |

The three `Location.*` codes are listed with meaning, trigger, and status under [Error codes: devices, playback, notifications, operations](/api/error-codes-devices-operations).

## Related

- [Concurrent changes: ETag and If-Match](/api/etag-and-if-match)
- [Commands for your organization](/cli/commands-organization)
- [Handbook: Locations](https://szenera.com/docs/displays/create-locations)
