[
  {
    "id": "index",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "Develop with Szenera",
    "anchor": "",
    "text": "Szenera’s API, CLI, Apps SDK, and webhooks: how to create keys, make calls, and receive events, plus the rules that apply on every route."
  },
  {
    "id": "index#whats-available",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "What’s available",
    "anchor": "whats-available",
    "text": "API. The HTTP API under /v1 is the foundation of everything: OpenAPI 3.1, 19 tags in seven groups, the reference at /api/reference and the specification at /api/openapi.json . Alongside it are the integrations that an external standard prescribes: delegated access through OAuth 2.1 and provisioning through SCIM. CLI. The szenera command line bundles 136 commands for organizations, with JSON output and exit codes. Every command is an API call. There is no public way to obtain the CLI today: anyone with the platform’s source tree builds it from there. Apps SDK. The SDK defines how an app is built (manifest, widget, backend, signature) and how it gets into the catalog. It is not published today, and the pages describe the contract as it applies in the platform’s source tree. Webhooks. Webhooks deliver your organization’s events to your systems: create an endpoint, verify the HMAC signature, understand retries, and retrieve the event catalog. Changes. What is stable in the API, where changes appear, and how you compare the specification against your own copy."
  },
  {
    "id": "index#tldr-in-five-commands",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "tl;dr in five commands",
    "anchor": "tldr-in-five-commands",
    "text": "The CLI is not shipped as a standalone package today. How you build it from the source tree is described under Get the CLI . Every command is an API call, which the page Your first call with curl also shows with curl."
  },
  {
    "id": "index#get-started",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "Get started",
    "anchor": "get-started",
    "text": "Area What you find there Getting started From API key to first call: create a key, query with curl, read responses, and make retries safe from the start."
  },
  {
    "id": "index#interfaces",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "Interfaces",
    "anchor": "interfaces",
    "text": "Area What you find there API The rules that apply on every route (address, authentication, errors, limits, paging, stability), the ways in for third-party applications, directory services, and assistants, and the guide through the reference. CLI The szenera command line: profiles, credentials, 136 commands, JSON output and exit codes, and what applies today when you obtain it. Webhooks Your organization’s events, delivered to your systems: create an endpoint, verify the signature, understand retries, look up event types, and retrieve the catalog."
  },
  {
    "id": "index#extend",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "Extend",
    "anchor": "extend",
    "text": "Area What you find there Apps SDK How an app is built (manifest, widget, backend, data access, signature) and how it gets into the catalog and onto the display. The whole path of a new app is under From template to release ."
  },
  {
    "id": "index#stay-up-to-date",
    "path": "",
    "area": "",
    "areaTitle": "Develop with Szenera",
    "page": "index",
    "pageTitle": "Develop with Szenera",
    "section": "Stay up to date",
    "anchor": "stay-up-to-date",
    "text": "Area What you find there Changes Where changes to the API originate, what a breaking change is, and how you compare the specification against your own copy, without promises that do not exist."
  },
  {
    "id": "getting-started/index",
    "path": "/getting-started",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "index",
    "pageTitle": "Getting started",
    "section": "Getting started",
    "anchor": "",
    "text": "From API key to first call: create a key, query with curl, read responses, and make retries safe from the start. The portal page Settings › Organization › API keys: the list of sections on the left, and on the right the Create key button and the table of issued keys with name and permissions, class, creator, creation time, validity, and last use This is where every integration starts: the key is created in the portal, and everything else goes through the API."
  },
  {
    "id": "getting-started/index#orientation",
    "path": "/getting-started",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "index",
    "pageTitle": "Getting started",
    "section": "Orientation",
    "anchor": "orientation",
    "text": "Page When you need it How does a call flow through the platform? From the bearer header through the permission check to the response with a trace ID: the stations every call passes through."
  },
  {
    "id": "getting-started/index#the-first-call",
    "path": "/getting-started",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "index",
    "pageTitle": "Getting started",
    "section": "The first call",
    "anchor": "the-first-call",
    "text": "Page When you need it Create an API key In the portal under Settings › Organization › API keys, create a key with class, permissions, and expiry. The plaintext appears exactly once. Your first call with curl Call GET /v1/me and GET /v1/playlists with a key and curl, and look at the response and its headers, in five minutes. What a response contains Status, headers, list shape, IDs with a prefix, and the error body: everything a response of the API holds, in tables. Idempotency from the start Protect write calls with Idempotency-Key, so that a retry after a network drop does not create a second operation."
  },
  {
    "id": "getting-started/index#if-you-get-stuck",
    "path": "/getting-started",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "index",
    "pageTitle": "Getting started",
    "section": "If you get stuck",
    "anchor": "if-you-get-stuck",
    "text": "Page When you need it When the first call fails 401, 403, 404, 422, 429, and network errors on the first call: what the code means and which page shows the way back."
  },
  {
    "id": "getting-started/about-request-flow",
    "path": "/getting-started/about-request-flow",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "about-request-flow",
    "pageTitle": "How does a call flow through the platform?",
    "section": "How does a call flow through the platform?",
    "anchor": "",
    "text": "Every call passes through the same three stations: the platform recognizes the credential, selects the operation under /v1 and checks its permission, and then answers with JSON and the headers that apply everywhere. Anyone who knows these stations can read every error response at a glance."
  },
  {
    "id": "getting-started/about-request-flow#station-1-the-credential-in-the-bearer-header",
    "path": "/getting-started/about-request-flow",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "about-request-flow",
    "pageTitle": "How does a call flow through the platform?",
    "section": "Station 1: The credential in the bearer header",
    "anchor": "station-1-the-credential-in-the-bearer-header",
    "text": "The platform recognizes from the prefix of the value in Authorization: Bearer … which kind of credential is present, with no further input from you: Prefix Credential For whom ak_ API key of an organization Machines: scripts, services, your integration eyJ Session token from POST /v1/auth/login People in the portal. Valid for 15 minutes, then a refresh oat_ Access token of a delegated access a third-party application on behalf of a person opk_ Key of platform operations only the operator of the platform rdr_ Renderer token the playback process of a paired device At the edges you meet sct_ (a directory service under /scim/v2 , nowhere else), inv_ (an invitation before the first sign-in), and dev_ (the first registration of a device). Devices in operation identify themselves with their certificate. For your integration, ak_… is the standard way: no expiry after a quarter of an hour, no refresh. A value with an unknown prefix explicitly does not count as anonymous. It is rejected as an unusable credential with 401 Auth.Required , so a typo in the prefix becomes a 401 and not a misleading 404. A call without a header gets the same response, and a key with the right prefix but a truncated value gets 401 Auth.TokenMalformed ."
  },
  {
    "id": "getting-started/about-request-flow#station-2-route-and-permission-check",
    "path": "/getting-started/about-request-flow",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "about-request-flow",
    "pageTitle": "How does a call flow through the platform?",
    "section": "Station 2: Route and permission check",
    "anchor": "station-2-route-and-permission-check",
    "text": "Every route that Szenera itself controls lives under /v1 , and the path is part of the contract. Only where a third-party standard dictates its address is the prefix absent ( /oauth/… , /.well-known/… , /scim/v2/… ). Then the platform checks, per operation, the permission that the reference names for it (for example playlist.list for GET /v1/playlists ). An API key carries the fixed list of permissions from its creation. Devices, pairing and deleting displays, and key management stay reserved for user accounts. If the permission is missing, the answer is 403 Permission.Denied . Routes of platform operations answer a key of an organization with 403 Permission.OperatorOnly . A call runs from the client through credential recognition, route, and permission check to the response. An unknown credential and a missing permission branch off into the error body The error branch carries the same body as every error: code, message, traceId."
  },
  {
    "id": "getting-started/about-request-flow#station-3-the-response-and-its-headers",
    "path": "/getting-started/about-request-flow",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "about-request-flow",
    "pageTitle": "How does a call flow through the platform?",
    "section": "Station 3: The response and its headers",
    "anchor": "station-3-the-response-and-its-headers",
    "text": "The platform speaks JSON. Four headers shape the exchange, three in the response and one in the request, wherever their condition is met: Header When X-Trace-Id always, the trace ID of this call If-Match (request) when changing versioned resources (scenes, playlists, programs, roles, displays): the version you read goes along as If-Match , and the same number is in the response as ETag Idempotent-Replay: true when a retry with the same Idempotency-Key received the stored response X-RateLimit-Limit , X-RateLimit-Remaining , X-RateLimit-Reset only on throttled routes, above all sign-in, registration, and password reset An error always comes in the same body: code in the form Area.Cause , message as a short text, and traceId with the prefix trc_ . The HTTP status follows the area of the code: Auth.* → 401, Permission.* → 403, *.NotFound → 404, Conflict.* → 409, Validation.* → 422, RateLimit.* → 429."
  },
  {
    "id": "getting-started/about-request-flow#what-follows",
    "path": "/getting-started/about-request-flow",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "about-request-flow",
    "pageTitle": "How does a call flow through the platform?",
    "section": "What follows",
    "anchor": "what-follows",
    "text": "Keys instead of sessions for machines. A session token expires after 15 minutes. A key ak_… does not. Branch on code, never on message. The code is stable and part of the contract. The text may change. Log the trace ID. X-Trace-Id and traceId are the same value, the one you give when you ask for help. Give the key only the permissions your integration needs. A key that is too narrow shows up as a 403 . A key that is too wide shows up as nothing."
  },
  {
    "id": "getting-started/about-request-flow#related",
    "path": "/getting-started/about-request-flow",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "about-request-flow",
    "pageTitle": "How does a call flow through the platform?",
    "section": "Related",
    "anchor": "related",
    "text": "Create an API key Authentication The error format"
  },
  {
    "id": "getting-started/create-api-key",
    "path": "/getting-started/create-api-key",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "create-api-key",
    "pageTitle": "Create an API key",
    "section": "Create an API key",
    "anchor": "",
    "text": "After this guide, you have a key ak_… with exactly the permissions your integration needs, and an expiry date. The plaintext appears exactly once, when you create the key."
  },
  {
    "id": "getting-started/create-api-key#steps",
    "path": "/getting-started/create-api-key",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "create-api-key",
    "pageTitle": "Create an API key",
    "section": "Steps",
    "anchor": "steps",
    "text": "Open Settings › Organization › API keys and click Create key . The Create API key dialog opens: “Set a name and permissions; the plaintext key is shown exactly once afterwards.” Under Name (1), enter what the key is for. The portal suggests “CI import” as an example. The name later appears in the list and in logs. Under Permissions (2), choose a Class . Read only is preselected: Class What the key may do Read only Fetch content, media, schedules, locations, displays, advertising, and proofs. Change nothing. Media automation Upload media and manage folders. Content authoring Create and change scenes and templates. Playout control Manage playlists and schedules. Full access Everything your account may do through the API. Custom Individual permissions, each explained. Below the cards, it says how many permissions the choice includes. For Read only with the full permissions of the Owner role, it reads “Includes 19 permissions.” The number is limited to your own permissions. With Adjust individual permissions (3), you open the selection from the permission catalog ( Search permission … , Add permission ). With Custom , it is open at once. Under Valid until (4), choose the lifetime: In 30 days , In 90 days (default), In one year , or No expiry . The list marks keys with no expiry separately. Under Old key valid until (5), choose how long the old value stays valid next to the new one when you rotate the key later: Invalid immediately , 1 hour , 24 hours (default), or 3 days . Click "
  },
  {
    "id": "getting-started/create-api-key#options",
    "path": "/getting-started/create-api-key",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "create-api-key",
    "pageTitle": "Create an API key",
    "section": "Options",
    "anchor": "options",
    "text": "Rotate. Actions › Rotate opens Rotate key : the key “gets a successor with the same permissions. The new key is shown to you once.” You choose how long the old one stays valid next to it ( Invalid immediately to 3 days ), and the message “Key rotated — the old one stays valid until …” names the time. Revoke. Actions › Revoke and the confirmation Revoke key? make the key invalid immediately and permanently. Running integrations lose access. Through the API. POST /v1/me/api-keys creates the same key (permission identity.api-key.create ). A key cannot issue another one, and the route expects a session token ( eyJ… ) from POST /v1/auth/login . The body has name , scope (list of permissions, at least one), expiresAt (mandatory, null means no expiry), and optionally originClass (default custom ) and rotationGraceHours (0–72, default 24). originClass records only the origin for display, so the permissions themselves must be listed in scope . The 201 response carries the plaintext once under plainKey . Fields and lifecycle: Authentication: keys and sessions . Through the command line. szenera api-key issue --name=\"Mediathek-Roboter\" --permissions=asset.list,asset.read . --list-allowed shows the allowed permissions. The command needs a profile with password sign-in, not a key profile (see Profiles and credentials ). All flags: Commands for your organization ."
  },
  {
    "id": "getting-started/create-api-key#check-the-result",
    "path": "/getting-started/create-api-key",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "create-api-key",
    "pageTitle": "Create an API key",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "The call GET /v1/me with the header Authorization: Bearer ak_… answers with 200 and your account (see Your first call with curl ). The list under Settings › Organization › API keys shows the new entry with Name / permissions , Class , Created by , Valid until , and Last used . At first it reads “Never used”, and after the first call it shows a timestamp. GET /v1/me/api-keys returns your own keys, also when called with the new key itself: the ID akm_… and the prefix ak_ plus nine characters, never the plaintext. GET /v1/api-keys shows all keys of the organization (permission identity.api-key.list )."
  },
  {
    "id": "getting-started/create-api-key#if-something-goes-wrong",
    "path": "/getting-started/create-api-key",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "create-api-key",
    "pageTitle": "Create an API key",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution Create key is disabled. Above it, it says “Existing keys keep working and can be revoked at any time. Issuing or renewing a key needs the … package.” The organization’s package does not include API access. Choose a package from Premium or agree on the enablement by contract. Existing keys keep working. The API answers POST /v1/me/api-keys with 402 Billing.AddonRequired The same through the API: the API access capability is missing. as above A class includes fewer permissions than expected The class is limited to your own permissions. An account with the missing permissions creates the key, or your role is extended. 403 Permission.Denied on POST /v1/me/api-keys with a key ak_… A key cannot issue a key. Use a session token or create the key in the portal. 403 ApiKey.ScopeEscalationBlocked The requested scope exceeds your own permissions. Reduce the scope. 422 ApiKey.ScopeNotKeyCapable A chosen permission is not meant for keys. details names it. Remove the permission. szenera api-key issue --list-allowed shows the permitted ones. You see only your own keys (“You are seeing your own keys. Other members' keys are reserved for accounts with administrative rights.”) You lack identity.api-key.list . An account with this permission sees all keys of the organization."
  },
  {
    "id": "getting-started/create-api-key#related",
    "path": "/getting-started/create-api-key",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "create-api-key",
    "pageTitle": "Create an API key",
    "section": "Related",
    "anchor": "related",
    "text": "Your first call with curl Authentication Developers area in the handbook"
  },
  {
    "id": "getting-started/first-call",
    "path": "/getting-started/first-call",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "first-call",
    "pageTitle": "Your first call with curl",
    "section": "Your first call with curl",
    "anchor": "",
    "text": "After this guide, you have called two routes with your key, your own account and your playlists, and seen the headers of a response. Five minutes, no installation except curl."
  },
  {
    "id": "getting-started/first-call#steps",
    "path": "/getting-started/first-call",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "first-call",
    "pageTitle": "Your first call with curl",
    "section": "Steps",
    "anchor": "steps",
    "text": "Store the address and key in environment variables. The key belongs in a header, never in the address. Addresses end up in logs, headers do not. Fetch your own account. GET /v1/me reads the user profile the key belongs to, and the response is shortened here to the fields that matter: tenantId is the ID of your organization. If a different one appears than the one you expected, the key points somewhere other than you intended. Fetch the list of your playlists. GET /v1/playlists needs the permission playlist.list , which the classes Read only , Playout control , and Full access include. A key of the class Media automation or Content authoring gets a 403 here. This response is also shortened to the most important fields: This list accepts limit between 1 and 200 (default 50) and pages through results with cursor . nextCursor is absent on the last page. You reach the displays ( GET /v1/players ) the same way with a key, because fleet.player.list is key-capable. You cannot reach the devices behind them ( GET /v1/devices ) that way, because identity.device.list is meant for user accounts only. A display is the surface that shows something. A device is the machine with certificate and pairing, and access to it stays reserved for people. Look at the headers. With -i , curl shows the status and the headers before the body: Two lines matter: Content-Type: application/json and X-Trace-Id: trc_… . The trace ID is the trace of this one call. Write it down when something is wrong."
  },
  {
    "id": "getting-started/first-call#the-same-call-with-the-command-line",
    "path": "/getting-started/first-call",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "first-call",
    "pageTitle": "Your first call with curl",
    "section": "The same call with the command line",
    "anchor": "the-same-call-with-the-command-line",
    "text": "The szenera command line takes address and key from environment variables and talks to the same routes. If standard output is not a terminal, it prints JSON. It reads the key from the same SZENERA_API_KEY as in step 1, and the address from SZENERA_BASE_URL : How to get the command line is described under Get the CLI ."
  },
  {
    "id": "getting-started/first-call#check-the-result",
    "path": "/getting-started/first-call",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "first-call",
    "pageTitle": "Your first call with curl",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "Both calls answer with 200 and a JSON body. Errors would come as a body with code , message , and traceId . The list of playlists carries items and, as long as more pages follow, nextCursor . In the portal, the key now appears under Settings › Organization › API keys with a timestamp in Last used ."
  },
  {
    "id": "getting-started/first-call#if-something-goes-wrong",
    "path": "/getting-started/first-call",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "first-call",
    "pageTitle": "Your first call with curl",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution 401 with Auth.Required The header is missing, Bearer is missing, or the value has no known prefix (a typo in the prefix). Check the header and the variable. See When the first call fails . 401 with Auth.TokenMalformed The prefix ak_ is right, but the key is truncated or contains invalid characters. Copy the full value from the secret store: ak_ plus 26 characters. 401 with Auth.ApiKeyUnknown , Auth.ApiKeyRevoked , or Auth.ApiKeyExpired The key does not exist, was revoked, or has expired. Create a new key. 403 with Permission.Denied The key does not carry the permission of the operation, or the route requires a permission that keys never carry (devices, pairing and deleting displays, key management). Adjust the class or permissions when creating the key. For these routes, use a session token. 404 with Resource.NotFound The path matches no route, usually because /v1 is missing or the base URL is wrong. Check address and path. 422 with Validation.Failed A parameter is invalid, such as limit=500 . Read details.issues and correct the value."
  },
  {
    "id": "getting-started/first-call#related",
    "path": "/getting-started/first-call",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "first-call",
    "pageTitle": "Your first call with curl",
    "section": "Related",
    "anchor": "related",
    "text": "What a response contains When the first call fails Paging"
  },
  {
    "id": "getting-started/response-contents",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "What a response contains",
    "anchor": "",
    "text": "This page is a lookup table: find what you encounter in a response (a status, header, list field, ID, or error field) in the tables, and read next to it what it means."
  },
  {
    "id": "getting-started/response-contents#status-classes",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Status classes",
    "anchor": "status-classes",
    "text": "Status Meaning What you do 200 , 201 Success. 201 when a resource is created. Evaluate the body. 302 Only on the sign-in flows through a provider, on cloud connections (round trip through the browser), and when fetching an app bundle (redirect to storage). Not relevant for ordinary calls with a key. curl follows with -L . 4xx Your call: credential, permission, path, body, or state do not fit. Always with an error body. Evaluate code , correct the call. 5xx The platform or a downstream service ( 502 ). Also with an error body. Report the traceId . For 502 with Retry-After , retry later."
  },
  {
    "id": "getting-started/response-contents#headers",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Headers",
    "anchor": "headers",
    "text": "Except for Idempotent-Replay , all headers named here are also readable for browser clients on a different origin. Header When Meaning Content-Type always application/json . With Accept: application/problem+json , errors come as application/problem+json X-Trace-Id always Trace ID of this call, trc_… , identical to traceId in the error body X-Correlation-Id if the call carries a correlation ID ID of a related operation, cor_… ETag on the read and change routes of versioned resources (scenes, playlists, programs, roles, displays) the version of the resource as a tag, such as \"7\" . When changing, send it back as If-Match (see Concurrent changes: ETag and If-Match ) Idempotent-Replay on a retry with the same Idempotency-Key true : the stored response was delivered, and the action did not run again X-RateLimit-Limit only on throttled routes requests allowed in the window X-RateLimit-Remaining only on throttled routes of those, still available X-RateLimit-Reset only on throttled routes start of the next window in Unix seconds Retry-After on 429 and on 502 with a wait time seconds until the next permitted attempt Server-Timing if the platform includes timing measurements duration of processing on the server"
  },
  {
    "id": "getting-started/response-contents#lists",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Lists",
    "anchor": "lists",
    "text": "Paged lists always have the same shape: Field Meaning items the records of this page nextCursor the marker for the next page, absent on the last page total the total count where the route knows it, otherwise the field is absent You page with the query parameters limit (page size) and cursor (value of nextCursor ). For paged lists, limit ranges from 1 to 200 with a default of 50. The filtered administration lists (devices, accounts, roles, keys) cap at 100 with a default of 25. Small lists that cannot be paged answer with a plain list, and the shape is stated at the endpoint in the reference."
  },
  {
    "id": "getting-started/response-contents#identifiers",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Identifiers",
    "anchor": "identifiers",
    "text": "Every ID consists of a prefix and 26 characters in the Crockford Base32 alphabet (digits and capital letters without I, L, O, and U), for example scn_01KJPX8NH0SZENE00000000001 . The prefix says what the ID denotes: Prefix Denotes tnt_ Organization usr_ User account dev_ Device ply_ Display ( playerId in the API) scn_ Scene pll_ Playlist sch_ Program (in the API scheduleId ) ast_ Media item fld_ Media folder rol_ Role whk_ Webhook endpoint whd_ Webhook delivery akm_ API key (the entry in lists) adv_ Advertiser acp_ Ad campaign bkl_ Booking of an ad campaign crv_ Creative of a booking ppr_ Plan revision of a display pss_ Screenshot of a display pbt_ Batch of plays that a device has reported pcl_ Closing of a booking over a period prf_ Proof of a closing pfi_ File of a proof (PDF, log, manifest, screenshot) pdy_ Delivery of a proof to a contact of the advertiser shl_ Link with which an advertiser opens their proofs without an account rvs_ Revenue statement of a location for a month trc_ Trace ID of a call cor_ Correlation ID of an operation The key itself begins with ak_ and is a secret value: it appears only once, when it is issued. Lists show its ID akm_… and the prefix ak_ plus nine characters for recognition."
  },
  {
    "id": "getting-started/response-contents#error-body",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Error body",
    "anchor": "error-body",
    "text": "Field When Meaning code always stable code in the form Area.Cause , such as Auth.Required or Conflict.OptimisticLock . Branch on this message always short text for humans. Do not parse it traceId always, never empty trace ID trc_… for follow-up questions details if the error carries additional information such as issues for Validation.Failed , expected / actual for Conflict.OptimisticLock . Absent otherwise correlationId if the call carries a correlation ID cor_… retryAfterSeconds , externalService only for integration errors ( 502 ) wait time (also as Retry-After ) and the downstream service concerned stack never in production only in development environments for internal errors The HTTP status follows the area of the code: Validation.* → 422, Auth.* → 401, Permission.* → 403, *.NotFound → 404, Conflict.* → 409, RateLimit.* → 429, Payload.* → 413, Integration.* → 502. Individual codes deviate explicitly, and the error code pages name the status actually sent for each code."
  },
  {
    "id": "getting-started/response-contents#errors-as-problem-details-rfc-9457",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Errors as Problem Details (RFC 9457)",
    "anchor": "errors-as-problem-details-rfc-9457",
    "text": "Send Accept: application/problem+json , and the same error comes as a ProblemDetails document: Field When Meaning type always address form of the code, lowercase with slashes title always short title of the code status always the HTTP status detail always the text of message code always the same code as in the standard body instance depending on the case the requested path errorDetails depending on the case the same additional information as details traceId , correlationId , retryAfterSeconds depending on the case as above There is never a stack in this form."
  },
  {
    "id": "getting-started/response-contents#timestamps",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Timestamps",
    "anchor": "timestamps",
    "text": "All timestamps are strings in ISO 8601 with a time zone offset, for example \"createdAt\": \"2026-05-04T07:00:00.000Z\" . A missing timestamp appears as null (such as \"expiresAt\": null for a key with no expiry)."
  },
  {
    "id": "getting-started/response-contents#related",
    "path": "/getting-started/response-contents",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "response-contents",
    "pageTitle": "What a response contains",
    "section": "Related",
    "anchor": "related",
    "text": "The error format Paging Concurrent changes: ETag and If-Match"
  },
  {
    "id": "getting-started/idempotency",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "Idempotency from the start",
    "anchor": "",
    "text": "After this guide, a retry after a network drop no longer creates a second operation: your client sends an Idempotency-Key with every write call, and the platform returns the stored response on a retry instead of a new resource."
  },
  {
    "id": "getting-started/idempotency#steps",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "Steps",
    "anchor": "steps",
    "text": "Generate a key for each operation, at least 8 characters long, ideally one that your system knows anyway, such as an order or import number. It only has to be unique within your organization for this method and this path. Send the key as the header Idempotency-Key with the write call: The platform stores the status, the body, and the headers ETag , Location , and X-Trace-Id of this first response for 24 hours. If the connection drops before the response reaches you, repeat the same call with the same key and the same body. No second folder is created. Recognize the retry by the header Idempotent-Replay: true : you received the stored response, and the action did not run again."
  },
  {
    "id": "getting-started/idempotency#rules-that-apply",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "Rules that apply",
    "anchor": "rules-that-apply",
    "text": "The cache is separate per organization, method, and path. The same key on a different path or with a different method hits a different entry. Shorter than 8 characters: 422 Validation.Failed . The same key with a different body is not a retry but a second operation under a borrowed name: 422 Validation.IdempotencyKeyReused . Neither the old response nor a new operation comes back. If a route enforces the key and it is missing: 409 Validation.IdempotencyKeyRequired . After 24 hours the entry has expired. The same key then runs the operation again. For changes to versioned resources (scenes, playlists, programs, roles, displays), If-Match with the version you read from the body is added. The idempotency key protects against double execution, and If-Match protects against overwriting other people’s changes. See Concurrent changes: ETag and If-Match ."
  },
  {
    "id": "getting-started/idempotency#through-the-command-line",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "Through the command line",
    "anchor": "through-the-command-line",
    "text": "szenera api post and the other write commands generate a fresh key for every call when you give none. Only to continue an aborted retry do you set it yourself:"
  },
  {
    "id": "getting-started/idempotency#check-the-result",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "The second call returns the same status and the same body as the first, with the same ID fld_… , and additionally Idempotent-Replay: true . The folder list contains the folder exactly once."
  },
  {
    "id": "getting-started/idempotency#if-something-goes-wrong",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution The second call does create a second resource A different path, a different method, a different key, or the entry expired after 24 hours. Compare the key, path, and method of the retry with the first attempt. Finish retries within 24 hours. 422 Validation.Failed with “Idempotency-Key too short” The key has fewer than 8 characters. Choose a longer key. 422 Validation.IdempotencyKeyReused The same key with a different body. For a new operation, issue a new key. 409 Validation.IdempotencyKeyRequired The route requires the key. Always send the header."
  },
  {
    "id": "getting-started/idempotency#related",
    "path": "/getting-started/idempotency",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "idempotency",
    "pageTitle": "Idempotency from the start",
    "section": "Related",
    "anchor": "related",
    "text": "Concurrent changes: ETag and If-Match The error format Scripts and CI"
  },
  {
    "id": "getting-started/troubleshooting",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "When the first call fails",
    "anchor": "",
    "text": "The symptoms are listed in the order in which they most often occur on the first call. Read the status first, then the field code in the error body, and keep the traceId in case you have to ask for help."
  },
  {
    "id": "getting-started/troubleshooting#symptoms-by-frequency",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "Symptoms by frequency",
    "anchor": "symptoms-by-frequency",
    "text": ""
  },
  {
    "id": "getting-started/troubleshooting#401-with-authrequired",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "401 with Auth.Required",
    "anchor": "401-with-authrequired",
    "text": "Meaning: The Authorization header is missing, the word Bearer before it is missing, or the value has no known prefix (a typo in the prefix). The platform never treats an unusable credential as anonymous. Even a value with an unknown prefix lands here and not on a 404. Solution: Check that the header reads exactly Authorization: Bearer ak_… and that the environment variable is set ( echo \"$SZENERA_API_KEY\" | cut -c1-3 must show ak_ ). Command line: exit code 3. See Your first call with curl ."
  },
  {
    "id": "getting-started/troubleshooting#401-with-authtokenmalformed",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "401 with Auth.TokenMalformed",
    "anchor": "401-with-authtokenmalformed",
    "text": "Meaning: The prefix ak_ is right, but the rest does not match the pattern of a key: exactly 26 characters made of digits and capital letters, without I, L, O, and U. A typical cause is a key truncated while copying, or a value with unexpected characters such as spaces, quotation marks, or lowercase letters. Solution: Copy the full value from your secret store and check the length ( echo -n \"$SZENERA_API_KEY\" | wc -c must print 29 ). Command line: exit code 3. See Your first call with curl ."
  },
  {
    "id": "getting-started/troubleshooting#401-with-authapikeyunknown-authapikeyrevoked-or-authapikeyexpired",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "401 with Auth.ApiKeyUnknown, Auth.ApiKeyRevoked, or Auth.ApiKeyExpired",
    "anchor": "401-with-authapikeyunknown-authapikeyrevoked-or-authapikeyexpired",
    "text": "Meaning: Prefix and pattern are right, but the key does not exist (a typo in the value that still fits the 26-character pattern), was revoked, or has passed its date under Valid until . Solution: Create a new key and replace the value in your secret store. A revoked key does not come back. See Create an API key ."
  },
  {
    "id": "getting-started/troubleshooting#401-with-authtokenexpired",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "401 with Auth.TokenExpired",
    "anchor": "401-with-authtokenexpired",
    "text": "Meaning: You are sending a session token ( eyJ… ) from the sign-in, and it expired after 15 minutes. Solution: For machines, use a key ak_… . If you need the sign-in flows, the refresh is described under Authentication ."
  },
  {
    "id": "getting-started/troubleshooting#403-with-permissiondenied",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "403 with Permission.Denied",
    "anchor": "403-with-permissiondenied",
    "text": "Meaning: The credential is valid but does not carry the permission of the operation. Each operation names its permission in the reference (such as identity.device.list ), and a key has only the permissions chosen when it was created. Some permissions keys never carry, such as identity.device.list (devices), fleet.playback-history.read (the daily playback history of a display), and identity.api-key.create . You reach these routes only with a session token. Solution: In the portal under Settings › Organization › API keys , check the columns Name / permissions and Class of the key. If the permission is missing, create a key with a suitable class or add it with Adjust individual permissions . Command line: exit code 4. See Create an API key ."
  },
  {
    "id": "getting-started/troubleshooting#403-with-permissionoperatoronly",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "403 with Permission.OperatorOnly",
    "anchor": "403-with-permissionoperatoronly",
    "text": "Meaning: The route under /v1/platform/… is open only to platform operations. No key of an organization ever gets in there. A new credential does not help here, but a different route does. Solution: Use the route of your organization (such as GET /v1/playlists instead of a route under /v1/platform ). See The areas of the reference ."
  },
  {
    "id": "getting-started/troubleshooting#402-with-billingaddonrequired",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "402 with Billing.AddonRequired",
    "anchor": "402-with-billingaddonrequired",
    "text": "Meaning: When creating or rotating a key: the organization’s package does not include API access. Existing keys keep working, and only new ones require the enablement. Solution: Choose a package from Premium (“API and webhooks”) or agree on the enablement of API access by contract. See Create an API key ."
  },
  {
    "id": "getting-started/troubleshooting#404-with-resourcenotfound",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "404 with Resource.NotFound",
    "anchor": "404-with-resourcenotfound",
    "text": "Meaning: The path matches no route. On the first call it is almost always one of three things: /v1 is missing ( /me instead of /v1/me ), there is a typo in the path, or the base URL points to the portal instead of the API. Solution: Check address and path. Every route begins with /v1 . Command line: exit code 5. See Base URL, /v1, and stability ."
  },
  {
    "id": "getting-started/troubleshooting#404-with-resourcenotfound-such-as-scenenotfound-devicenotfound",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "404 with <Resource>.NotFound (such as Scene.NotFound, Device.NotFound)",
    "anchor": "404-with-resourcenotfound-such-as-scenenotfound-devicenotfound",
    "text": "Meaning: The route is right, but the ID does not belong to your organization or no longer exists. An ID from another organization cannot be told apart from a made-up one: both give the same 404. Solution: Take the ID from a list of your organization ( GET /v1/scenes , GET /v1/playlists , …) and match the prefix to the resource ( scn_ for scenes, pll_ for playlists). See What a response contains ."
  },
  {
    "id": "getting-started/troubleshooting#422-with-validationfailed",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "422 with Validation.Failed",
    "anchor": "422-with-validationfailed",
    "text": "Meaning: A parameter or a field in the body violates the contract, such as limit=500 where at most 200 applies, a missing required field, or a wrong type. Solution: Read details.issues : each entry names the field path and the violation. Command line: exit code 7. The command line checks a body you wrote yourself before sending and reports the field path with exit code 1. See The error format ."
  },
  {
    "id": "getting-started/troubleshooting#429-with-ratelimitexceeded",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "429 with RateLimit.Exceeded",
    "anchor": "429-with-ratelimitexceeded",
    "text": "Meaning: Too many requests in one window. The routes most prone to abuse are throttled above all: sign-in, registration, and password reset. A few other routes say so in the reference. Your first call with a key does not hit them, but anyone who repeats the sign-in in a loop does. Solution: The header Retry-After gives the seconds until the next permitted attempt, and X-RateLimit-Reset the start of the next window. Wait, then retry, and for machines use a key instead of the sign-in. Command line: exit code 8. See Throttling and rate-limit headers ."
  },
  {
    "id": "getting-started/troubleshooting#no-http-response-connection-refused-timeout-tls-error",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "No HTTP response: connection refused, timeout, TLS error",
    "anchor": "no-http-response-connection-refused-timeout-tls-error",
    "text": "Meaning: The call did not reach the platform. The base URL is wrong, the port is missing or wrong, the certificate does not match the name, or a proxy blocks it. Solution: First check the address without a credential. szenera health queries the public readiness route of your environment and does not fail on the sign-in, so a wrong port does not look like a wrong key. Command line: exit code 10. Then step 1 under Your first call with curl ."
  },
  {
    "id": "getting-started/troubleshooting#keep-the-trace-id",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "Keep the trace ID",
    "anchor": "keep-the-trace-id",
    "text": "Every error response carries traceId in the body and X-Trace-Id in the header, the same value trc_… . It is the trace of exactly this call on the platform. When you ask for help, name it. Without it, a single call cannot be found again."
  },
  {
    "id": "getting-started/troubleshooting#related",
    "path": "/getting-started/troubleshooting",
    "area": "getting-started",
    "areaTitle": "Getting started",
    "page": "troubleshooting",
    "pageTitle": "When the first call fails",
    "section": "Related",
    "anchor": "related",
    "text": "Create an API key Your first call with curl Error codes: general"
  },
  {
    "id": "api/index",
    "path": "/api",
    "area": "api",
    "areaTitle": "API",
    "page": "index",
    "pageTitle": "API",
    "section": "API",
    "anchor": "",
    "text": "The rules that apply on every route (address, authentication, errors, throttling, paging, stability) and the guide through the reference. The portal dialog “Create API key” with a filled-in name field, the six permission classes (“Read only” is selected, with 19 permissions), and the selection fields for the validity of the new and the old key The rules of this area apply to calls made with an ak_… key. Keys are issued, rotated, and revoked in the portal under Settings › API keys."
  },
  {
    "id": "api/index#basics",
    "path": "/api",
    "area": "api",
    "areaTitle": "API",
    "page": "index",
    "pageTitle": "API",
    "section": "Basics",
    "anchor": "basics",
    "text": "Page When you need it Base URL, /v1, and stability Where the base URL comes from, why almost every route carries /v1, which paths lie outside it, and what x-stability promises per operation. Authentication: keys and sessions Bearer header, key prefixes, permission scope, expiry and rotation of a key, and when a session makes more sense than a key. The error format The error body with code, message, and traceId, the RFC 9457 form, and the rule by which a code becomes the HTTP status. Concurrent changes: ETag and If-Match Read a versioned resource, send the version back as If-Match, and resolve a 409 conflict cleanly. Throttling and rate-limit headers Which routes have a limit of their own, which rate limit is attached to a key, what the four headers mean, and how a client reacts to 429. Paging: limit, cursor, items, nextCursor Cursor paging on all lists, the limits 50/200 and 25/100, the four routes with page, and how the CLI follows all pages with --all."
  },
  {
    "id": "api/index#access-methods-and-resources",
    "path": "/api",
    "area": "api",
    "areaTitle": "API",
    "page": "index",
    "pageTitle": "API",
    "section": "Access methods and resources",
    "anchor": "access-methods-and-resources",
    "text": "Page When you need it Delegated access with OAuth 2.1 Your application runs at other customers’ sites: register, have users consent with PKCE, exchange the oat_ token, renew, revoke. Provisioning with SCIM 2.0 A directory service should create accounts, suspend them, and set roles: token sct_, endpoints under /scim/v2, mapping, and errors. The MCP service for AI assistants An assistant should work with your inventory: address, authentication, tool list, confirmation requirement, limits, and errors. Locations Mirror a branch structure: fields of a location, its routes, assigning a display, and analytics by location. Web displays Create a display with no device behind it: routes, connection URL, the two states, deletion, and the limits compared with a device."
  },
  {
    "id": "api/index#error-codes",
    "path": "/api",
    "area": "api",
    "areaTitle": "API",
    "page": "index",
    "pageTitle": "API",
    "section": "Error codes",
    "anchor": "error-codes",
    "text": "Page When you need it Error codes: possible on any route The platform-wide codes for authentication, permissions, validation, conflicts, throttling, payload, HTTP, events, caching, jobs, and resources. Error codes: accounts and sessions The codes of the areas Identity and Sessions: user accounts, keys, invitations, sign-in, second factor, sessions. Error codes: organization, access, joining The codes of the areas Tenancy, Access, Onboarding, and Cloud-Connections: master data, domain, roles, grants, registration, storage connections. Error codes: billing The codes of the area Billing: package, licenses, add-ons, payment method, invoices, and why 402 appears here. Error codes: media, scenes, programs, apps The codes of the areas Media-Library, Composition, Scheduling, App-Catalog, Marketplace, App-Runtime, and Advertising. Error codes: devices, playback, notifications, operations The codes of the areas Fleet, Playback, Realtime, Notifications, Analytics, Audit, and Ops-Console, the latter only on operator routes. Error codes: server errors (5xx) The 119 codes to which a client cannot react in a domain-specific way: invariants, startup, database, downstream services. Retry or report."
  },
  {
    "id": "api/index#guide-to-the-reference",
    "path": "/api",
    "area": "api",
    "areaTitle": "API",
    "page": "index",
    "pageTitle": "API",
    "section": "Guide to the reference",
    "anchor": "guide-to-the-reference",
    "text": "Page When you need it Read the reference Find an operation at /api/reference, read off permission, security scheme, example, and stability, and download the specification. The areas of the reference The 19 tags of the specification in their 7 groups with one sentence of purpose each, and why the operator surface is not part of it. Generate your own client from the specification There is currently no published client. Here is how you generate your own from openapi.json and map the conventions of the API into it."
  },
  {
    "id": "api/base-url-and-stability",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "Base URL, /v1, and stability",
    "anchor": "",
    "text": "Where the base URL comes from, why almost every route carries /v1 , what the specification says about itself, and what x-stability guarantees per operation. Read the tables as a contract: Nothing stated here changes without notice."
  },
  {
    "id": "api/base-url-and-stability#base-url",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "Base URL",
    "anchor": "base-url",
    "text": "The API has no fixed public address. You address the origin of your environment. The specification on this site names it as its first server under the name “Die Schnittstelle Ihrer Umgebung” (the API of your environment). The operator fills in the address at startup, which is why a self-built version still shows the placeholder __SZENERA_API_BASE_URL__ in its place. As its second server, it lists http://localhost:41000 for a local development environment. The version served by your environment ( GET /v1/openapi.json ) names / instead: There, its own origin is the right answer, whereas here it would be this documentation site. What Value Base URL the origin of your environment, e.g., https://api.<your-domain> Path prefix /v1 on every route Transport HTTPS. Request and response bodies as JSON Example GET $SZENERA_API/v1/me In all examples on these pages, the base URL is stored in the environment variable SZENERA_API : Where set up, sign-in has a second host: https://auth.<your-domain> . It serves the same interface for /v1/auth/… , OAuth, SCIM and the metadata, while all other routes stay at the base URL. The sign-in endpoints will in future be named by the metadata of auth.<your-domain> , and the addresses at the base URL remain reachable until further notice."
  },
  {
    "id": "api/base-url-and-stability#the-v1-prefix",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "The /v1 prefix",
    "anchor": "the-v1-prefix",
    "text": "Every route that Szenera itself decides on lives under /v1 , and the path is part of the contract: GET /v1/scenes/{id} , POST /v1/auth/login , GET /v1/devices . Four groups sit outside the prefix, and none of them is an oversight. Path Why without /v1 /scim/v2/… The path belongs to a third-party protocol (RFC 7644), which carries its own version. A directory service validates its data against the address it talks to, and a prefix in front would break that. See Provisioning with SCIM 2.0 . /.well-known/oauth-authorization-server , /.well-known/oauth-protected-resource RFC 8414 and RFC 9728 define these addresses literally. A third-party client looks there and nowhere else. /oauth/register , /oauth/authorize , /oauth/token , /oauth/revoke the endpoints of the standard, see Delegated access with OAuth 2.1 GET /health/live , GET /health/ready , GET /health/version , GET /metrics operational routes that need no sign-in. They describe the state of an environment, not your inventory, and are therefore not part of the public specification either Everything that lies behind the consent, meaning the consent itself and the connected applications under GET /v1/me/connected-apps , lives under /v1 as usual. These routes are the contract of this API, not that of a third-party standard."
  },
  {
    "id": "api/base-url-and-stability#the-specification",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "The specification",
    "anchor": "the-specification",
    "text": "Detail Value Format OpenAPI 3.1.0 info.title Szenera API info.version 1.0.0 Operations 443 on 346 paths Areas (tags) 19 in 7 groups, see The areas of the reference Schemas with example 741 of 741 Two sources: Source Content Access /api/openapi.json on this site the current release, without the operations marked internal no sign-in required GET /v1/openapi.json on your environment the same public version, for the build that is actually running there no sign-in required. Cache-Control: public, max-age=300 , so it can be cached for five minutes Both sources show the same selection: the operations of the classes stable and beta . Whatever carries internal drops out, together with the schemas that were needed only there. Today that is 337 of the 780 operations, including the entire operator surface under /v1/platform/… . A client generated from it therefore gets only what your organization may also call. The full version is available to the platform operator behind its own sign-in. It does not appear on this site."
  },
  {
    "id": "api/base-url-and-stability#stability-per-operation",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "Stability per operation",
    "anchor": "stability-per-operation",
    "text": "Every operation carries the extension x-stability with one of three values: Value Meaning stable Path, required fields, and error codes do not change without a new version. beta Public but young: The shape can still move until it has survived one cycle without a breaking change. internal Not part of the public surface. Appears only in the operator’s version and therefore in no file on this site. State of the current release: 372 of the 443 operations are stable , and 71 are beta : the 31 routes of advertising under /v1/advertising/… (see The areas of the reference ), GET /v1/webhooks/event-types (see Fetch the event catalog through the API ), the dry run POST /v1/scenes/validate (see The MCP service for AI assistants ), the three routes of the render job of a scene version ( POST /v1/scenes/{id}/versions/{vid}/render , GET /v1/scenes/{id}/versions/{vid}/render/{jobId} , and GET /v1/scenes/{id}/versions/{vid}/render/{jobId}/image ), the five routes of the web displays , whose shape has already shifted once in this wave, the two routes of the screenshots of a display ( GET /v1/players/{playerId}/screenshots and GET /v1/players/{playerId}/screenshots/{screenshotId} ), the nine routes of the new analytics under /v1/analytics/ (the ad hoc analysis with catalog and output, the overview, the day and the log of a display, the content, and the delivery of an ad campaign), the eight routes of the saved views under /v1/analytics/reports , which changed shape with the new analytics, and "
  },
  {
    "id": "api/base-url-and-stability#what-does-not-exist",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "What does not exist",
    "anchor": "what-does-not-exist",
    "text": "No version selection through headers or query parameters. The version is in the path. No separate base URL per area: All areas live under the same origin, including the paths without /v1 . Sign-in is additionally reachable at the sign-in host, with the same paths. No second contract next to this specification: Whatever the command line, the assistant, or a portal can do, this API can do too. When an operation is retired, it carries deprecated: true and x-sunset with the shutdown date, and its responses carry Deprecation and Sunset . Twelve months separate the announcement from the shutdown. The details are described under Stability classes and sunset . How changes to the API appear and how you compare two versions of the specification is described under Release trains, notes, and deprecations ."
  },
  {
    "id": "api/base-url-and-stability#related",
    "path": "/api/base-url-and-stability",
    "area": "api",
    "areaTitle": "API",
    "page": "base-url-and-stability",
    "pageTitle": "Base URL, /v1, and stability",
    "section": "Related",
    "anchor": "related",
    "text": "Read the reference Your first call with curl Stability classes and sunset"
  },
  {
    "id": "api/authentication",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "Authentication: keys and sessions",
    "anchor": "",
    "text": "Every call identifies itself with the Authorization: Bearer <value> header, and the API recognizes the type of credential from the prefix of the value. For integrations, the API key is the default choice. This page lists the prefixes, the rules for keys, their lifecycle, and when a session is the better choice."
  },
  {
    "id": "api/authentication#the-prefixes",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "The prefixes",
    "anchor": "the-prefixes",
    "text": "Prefix Credential For integrations ak_ API key of an organization yes, the default for your own server oat_ Access token of a delegated access grant yes, when a person gives your application permissions, see Delegated access with OAuth 2.1 eyJ Access token of a session for user interfaces and the command line with a password sct_ SCIM token of an organization only for /scim/v2 , see Provisioning with SCIM 2.0 opk_ Operator key platform operations only rdr_ Renderer token of a paired device no inv_ Invitation token before the first sign-in no dev_ Device credential for the initial registration no ak_ and opk_ are distinct prefixes: opk_ does not start with ak_ . A key therefore says by itself which kind it is. Do not confuse the secret values with the identifiers of their records ( akm_ , sctm_ , oac_ ). Identifiers appear in lists and logs, while the secret value itself appears exactly once, in the response that issues it. An sct_ token is accepted on no route other than /scim/v2 and is rejected with 403 everywhere else. Conversely, neither an ak_ nor an oat_ credential reaches the SCIM surface. A value with an unknown prefix counts as unknown , not as anonymous: The response is 401 , not 404 . Without the header, you reach only public routes, for example GET /v1/openapi.json , GET /health/ready , or POST /v1/auth/login . In the reference, you recognize them by an empty security list and the sentence “Öffentlich — keine Anmeldung erforderlich” (public, no sign-in required). E"
  },
  {
    "id": "api/authentication#the-api-key",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "The API key",
    "anchor": "the-api-key",
    "text": "Rule Value Form ak_ and 26 characters, e.g., ak_01KQRWJRC0APKEY00000000001 Plaintext appears exactly once, in the response to POST /v1/me/api-keys (field plainKey ) Recognition Lists show only keyPrefix ( ak_ and 9 characters) and the identifier akm_… Lifetime does not expire after 15 minutes. expiresAt sets an expiration date, null means no expiry Permissions a list of specific permissions ( scope ). A class ( read-only , media-automation , content-authoring , playout-control , full ) only selects this list, and custom is the free choice. What gets sent is always scope , and the list is not kept in sync with the class afterward Upper limit a key can never do more than its creator ( 403 ApiKey.ScopeEscalationBlocked ) Key capability only permissions that the catalog provides for keys ( 422 ApiKey.ScopeNotKeyCapable ) Organization a key belongs to exactly one organization GET /v1/permission-catalog tells you which permissions a key can carry. It needs no special permission, only authentication. Every entry carries appliesTo , and only permissions with apiKey in this list are key-capable. In the core catalog, that is currently 56 permissions: media ( asset.* ), scenes and layout templates ( scene.* , layout-template.* ), playlists and programs ( playlist.* , schedule.* ), locations ( location.* ), the displays ( fleet.player.list , fleet.player.read , fleet.player.update , fleet.player.command.issue ), the web displays ( fleet.web-display.manage ), advertising (all eight advert"
  },
  {
    "id": "api/authentication#key-lifecycle",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "Key lifecycle",
    "anchor": "key-lifecycle",
    "text": "Action Route Permission Note Create POST /v1/me/api-keys identity.api-key.create Body: name , scope (required, list of key-capable permissions), expiresAt (required, null = no expiry). Optional: originClass (label of the chosen class, default custom ), rotationGraceHours (0–72, default 24), and rateLimit (the key’s own rate limit in the form 600/1m , null = the default of the environment, see Throttling ). The class only selects the permission list, and what gets sent is always scope Read your own keys GET /v1/me/api-keys Session only keys you created yourself. Called with a key, the route answers 401 All keys of the organization GET /v1/api-keys identity.api-key.list including those of former members. Parameters: includeRevoked , createdById , search , sort , order Rotate POST /v1/me/api-keys/{id}/rotate identity.api-key.create Body graceHours 0–72. The successor takes over the name, permissions, and rate limit, and the old key stays valid until oldRevokesAt Revoke DELETE /v1/me/api-keys/{id} identity.api-key.revoke Body reason : owner-rotation (no longer needed or replaced) or compromised (probably in the wrong hands). Takes effect immediately and cannot be undone The response, with both key objects trimmed to the fields an integration needs: In the portal, you find the same actions under Settings › Organization › API keys with the buttons Create key , Rotate , and Revoke . The grace period is called Old key valid until there, with the options Invalid immediately , 1 hour ,"
  },
  {
    "id": "api/authentication#the-session",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "The session",
    "anchor": "the-session",
    "text": "A session is the way in for user interfaces and for people at the command line, where it is called credential type password . It needs refreshing, a key does not. Step Route Body Result Sign in POST /v1/auth/login email , password , audience ( tenant ), deviceFingerprint 201 with outcome : session , mfa-required , or organization-choice Second factor POST /v1/auth/mfa/verify mfaSessionId , code outcome: session Refresh POST /v1/auth/refresh no body, the cookie travels with the request new access token Component Rule Access token accessToken in the body, expiresAt gives the expiry. Lifetime 15 minutes. Travels as Authorization: Bearer eyJ… Refresh cookie refresh_token , httpOnly , path /v1/auth , SameSite=Lax , 7 days. Only POST /v1/auth/refresh reads it and renews it Cookie rule A cookie is set only if the response is actually a session ( outcome: session ) Other origins Browser clients send credentials: 'include' , otherwise the cookie stays behind Cookie host The cookie carries no Domain attribute and belongs to the host that sets it: sign-in and refresh go to the same host. If your environment has a sign-in host ( auth.<your-domain> ), it is that one Regular API requests are not authenticated with cookies. The Bearer header is always the credential. If an account belongs to several organizations, sign-in ends in organization-choice . A key does not face this question because it belongs to exactly one organization. Company sign-in through your organization’s provider and th"
  },
  {
    "id": "api/authentication#key-or-session",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "Key or session?",
    "anchor": "key-or-session",
    "text": "Situation Choice Server-to-server, scripts, CI Key Permissions for media, scenes, playlists, programs, locations, advertising, and proofs Key Controlling devices, managing accounts or webhooks Session, because no key carries these permissions A quick lookup without creating a key Session through the command line with credential type password Your application runs at other customers’ sites and should see their inventory Delegated access oat_… : a person consents instead of handing over a key Maintaining accounts from an HR directory SCIM token sct_…"
  },
  {
    "id": "api/authentication#related",
    "path": "/api/authentication",
    "area": "api",
    "areaTitle": "API",
    "page": "authentication",
    "pageTitle": "Authentication: keys and sessions",
    "section": "Related",
    "anchor": "related",
    "text": "Create an API key Delegated access with OAuth 2.1 Provisioning with SCIM 2.0 Error codes: accounts and sessions Handbook: Organization"
  },
  {
    "id": "api/error-format",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "The error format",
    "anchor": "",
    "text": "Every error of the API comes back in the same body: code , message , and traceId always, further fields if applicable. This page lists the fields, the rule by which a code becomes the HTTP status, the seven central responses of the reference, and the RFC 9457 form."
  },
  {
    "id": "api/error-format#the-error-body",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "The error body",
    "anchor": "the-error-body",
    "text": "Field When Content code always the stable error code in the form Area.Cause , pattern ^[A-Z][a-zA-Z]+\\.[A-Z][a-zA-Z0-9]+$ message always short description for humans, 1–1000 characters, may change traceId always the trace of this call: trc_ and 26 characters, never empty and identical to the X-Trace-Id header details if applicable additional information without personal data: issues for input errors, expectedVersion and actualVersion for version conflicts. Omitted entirely when there is nothing to report correlationId if applicable identifier of a connected process, cor_… , up to 128 characters retryAfterSeconds for downstream services whole seconds until the next attempt. Also sent as the Retry-After header externalService for downstream services name of the service that returned an unusable response stack never in production only in development environments, for server errors Two rules for every client: Branch on code , never on message . And include the traceId when you ask for help, because it is the key that lets the operations team find the call again."
  },
  {
    "id": "api/error-format#how-the-http-status-follows-from-the-code",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "How the HTTP status follows from the code",
    "anchor": "how-the-http-status-follows-from-the-code",
    "text": "The status is not set freely but derived deterministically from the code. The resolution checks in a fixed order, and the first match wins. Step Rule Example 1 Exact mapping of the individual code Validation.IdempotencyKeyRequired → 409, Http.BadRequest → 400 2 Mapping of the functional area Billing.LicensesExhausted → 402, Tenant.Frozen → 403, Signup.TicketInvalid → 401 3 Convention of the namespace Validation.* → 422, Auth.* → 401, Permission.* → 403, *.NotFound → 404, Conflict.* → 409, RateLimit.* → 429, Payload.* → 413, Integration.* → 502 4 Otherwise 500 That is why most server errors carry no mapping of their own: They deliberately fall through to 500. Where a domain code deviates from the convention, that is intentional and explained on the respective code page."
  },
  {
    "id": "api/error-format#the-seven-central-responses",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "The seven central responses",
    "anchor": "the-seven-central-responses",
    "text": "The reference describes seven responses centrally. They mean the same on every operation. Response Status Meaning Unauthorized 401 missing or invalid authentication PermissionDenied 403 authenticated, but without the required permission NotFound 404 the resource does not exist or is not visible to you Conflict 409 concurrent change or violation of a uniqueness constraint ValidationFailed 422 the request violates the schema or a business rule RateLimit 429 too many requests, Retry-After gives the wait time IntegrationError 502 a downstream service returned an unusable response"
  },
  {
    "id": "api/error-format#rfc-9457-problem-details",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "RFC 9457: Problem Details",
    "anchor": "rfc-9457-problem-details",
    "text": "If you send Accept: application/problem+json , the API responds with Content-Type: application/problem+json and a ProblemDetails document instead of the standard body. Field When Content type always the address form of the code title always short title, up to 200 characters status always the HTTP status as a number detail always the description, up to 1000 characters code always set the same error code as in the standard body instance if applicable the requested path errorDetails if applicable the same additional information as details traceId , correlationId , retryAfterSeconds if applicable as in the standard body There is no stack in this form."
  },
  {
    "id": "api/error-format#where-the-codes-are-listed",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "Where the codes are listed",
    "anchor": "where-the-codes-are-listed",
    "text": "The catalog contains 612 codes, spread across seven pages: Page Areas Possible on any route authentication, permissions, input validation, conflicts, throttling, payload, HTTP, operator routes Accounts and sessions accounts, keys, devices, sign-in, second factor Organization, access, joining master data, roles, invitations, registration, storage connections Billing package, licenses, payment method, invoices Media, scenes, programs, apps media, scenes, playlists, programs, app catalog, marketplace, app runtime, news sources, advertising Devices, playback, notifications, operations fleet, playback, realtime, webhooks, analytics, audit log, operations console Server errors (5xx) invariants, startup, database, downstream services"
  },
  {
    "id": "api/error-format#related",
    "path": "/api/error-format",
    "area": "api",
    "areaTitle": "API",
    "page": "error-format",
    "pageTitle": "The error format",
    "section": "Related",
    "anchor": "related",
    "text": "Error codes: possible on any route What a response contains Concurrent changes: ETag and If-Match"
  },
  {
    "id": "api/etag-and-if-match",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "Concurrent changes: ETag and If-Match",
    "anchor": "",
    "text": "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."
  },
  {
    "id": "api/etag-and-if-match#how-the-version-check-works",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "How the version check works",
    "anchor": "how-the-version-check-works",
    "text": "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."
  },
  {
    "id": "api/etag-and-if-match#steps",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "Steps",
    "anchor": "steps",
    "text": "Read the resource and note the version, from the header or from the body: Send the change with If-Match and exactly this value: The response is the scene with the new version (shortened here to four fields): A patch with identical content leaves the version as it is. If 409 comes back, someone wrote between steps 1 and 2: 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."
  },
  {
    "id": "api/etag-and-if-match#options",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "Options",
    "anchor": "options",
    "text": "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 "
  },
  {
    "id": "api/etag-and-if-match#check-the-result",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "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 ."
  },
  {
    "id": "api/etag-and-if-match#if-something-goes-wrong",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "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."
  },
  {
    "id": "api/etag-and-if-match#related",
    "path": "/api/etag-and-if-match",
    "area": "api",
    "areaTitle": "API",
    "page": "etag-and-if-match",
    "pageTitle": "Concurrent changes: ETag and If-Match",
    "section": "Related",
    "anchor": "related",
    "text": "Idempotency from the start The error format Commands for your organization"
  },
  {
    "id": "api/rate-limits",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "Throttling and rate-limit headers",
    "anchor": "",
    "text": "Two things are throttled, for two reasons: The abuse-prone routes (sign-in, sign-in link, registration, password reset, and the round trips of provider sign-in) each carry a limit of their own, and every call with an API key also counts against a rate limit that belongs to the key. This page lists the headers, both limits, and the right reaction to 429 ."
  },
  {
    "id": "api/rate-limits#the-headers",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "The headers",
    "anchor": "the-headers",
    "text": "Every response to which a limit applies carries the state of the window: Header Meaning X-RateLimit-Limit requests allowed in the window in total X-RateLimit-Remaining of those, how many are still open X-RateLimit-Reset the point in time at which the window starts anew, in Unix seconds Retry-After seconds until the next permitted attempt, only when the limit is exceeded ( 429 ) All four are readable for browser clients on other origins, as are X-Trace-Id , X-Correlation-Id , and Server-Timing ."
  },
  {
    "id": "api/rate-limits#the-response-when-a-limit-is-exceeded",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "The response when a limit is exceeded",
    "anchor": "the-response-when-a-limit-is-exceeded",
    "text": "The status is 429 , with Retry-After . A second code of the same area, RateLimit.GlobalCircuitBreaker (also 429 ), indicates a platform-wide overload safeguard. The reaction is the same."
  },
  {
    "id": "api/rate-limits#the-limits-of-the-sign-in-and-registration-routes",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "The limits of the sign-in and registration routes",
    "anchor": "the-limits-of-the-sign-in-and-registration-routes",
    "text": "The limits apply per window and per counting key: “IP” counts per sender address, and “IP+email” counts per sender address and email address in the body. Route Limit POST /v1/auth/identify 10 per minute (IP+email) and 40 per 10 minutes (IP) POST /v1/auth/login 5 per minute (IP+email) POST /v1/auth/login/choose-organization 10 per minute (IP) POST /v1/auth/mfa/verify , POST /v1/auth/mfa/recovery-code 5 per minute (IP) POST /v1/auth/login/magic-link 5 per minute (IP+email) POST /v1/auth/login/magic-link/consume 5 per minute (IP) POST /v1/auth/password-reset/request 5 per minute (IP+email) POST /v1/auth/password-reset/consume 5 per minute (IP) POST /v1/auth/onboarding/consume 5 per minute (IP) GET /v1/auth/login/sso/{providerId} 20 per minute (IP) GET|POST /v1/auth/login/sso/callback 30 per minute (IP) GET /v1/auth/login/social 60 per minute (IP) GET /v1/auth/login/social/{provider} 20 per minute (IP) GET|POST /v1/auth/login/social/callback 30 per minute (IP) POST /v1/auth/login/exchange 10 per minute (IP) POST /v1/auth/sso-recovery/consume 5 per minute (IP) GET /v1/auth/signup 60 per minute (IP) POST /v1/auth/signup/start 3 per 10 minutes (IP+email) and 10 per 24 hours (IP) POST /v1/auth/signup/verify , POST /v1/auth/signup/social 10 per minute (IP) POST /v1/auth/signup/complete , POST /v1/auth/signup/join 5 per minute (IP) POST /v1/tenants/me/sso/{providerId}/domains/{domain}/verify 10 per minute (IP) Further public and session-bound routes have windows of their own, for examp"
  },
  {
    "id": "api/rate-limits#the-rate-limit-per-key",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "The rate limit per key",
    "anchor": "the-rate-limit-per-key",
    "text": "A call with an ak_… key counts against a rate limit that belongs to the key, on every route, including the read routes without a limit of their own. The reason is the nature of an automated client: A person clicks, a script queries in a loop, and the cheap read routes are what knock a stack over fastest. What Value Default a value of the environment in the form <count>/<window> , such as 600/1m . The window is given in s , m , or h Per key can be overridden at creation (field rateLimit in POST /v1/me/api-keys , where null means “the default of the environment”), and a rotation carries the value over Counted per key, not per sender address and not per organization Headers the same four as above, on every response When exceeded 429 RateLimit.Exceeded with Retry-After Calls from a signed-in session, devices, renderers, and operator keys are not affected: They have limits of their own. If several rules apply to a call, for example the limit of the sign-in route and the rate limit of the key, the headers show the tightest one, and the rule that was exceeded first determines the error code. What a client does with this: Track X-RateLimit-Remaining and slow your own loop before zero is reached. An import that creates ten thousand media items finishes faster with a small pause per call than one that runs into the wall and waits."
  },
  {
    "id": "api/rate-limits#the-right-reaction",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "The right reaction",
    "anchor": "the-right-reaction",
    "text": "Rule Why Wait for Retry-After , then retry exactly once Every attempt before that counts toward the window and extends the lockout. Watch X-RateLimit-Remaining and slow down before zero A client that knows the window never sees a 429 . Do not hammer Retries every second fill the window instead of emptying it. When a call gets 429 , the command line exits with code 8 ( rate-limited ), see JSON output and exit codes ."
  },
  {
    "id": "api/rate-limits#related",
    "path": "/api/rate-limits",
    "area": "api",
    "areaTitle": "API",
    "page": "rate-limits",
    "pageTitle": "Throttling and rate-limit headers",
    "section": "Related",
    "anchor": "related",
    "text": "The error format JSON output and exit codes"
  },
  {
    "id": "api/pagination",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Paging: limit, cursor, items, nextCursor",
    "anchor": "",
    "text": "Lists in the API are paged with a cursor: limit bounds the page, cursor continues it, and the response carries items and, as long as more records follow, nextCursor . This page lists the limits, the four exceptions with page , and the loop that collects all pages."
  },
  {
    "id": "api/pagination#cursor-paging",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Cursor paging",
    "anchor": "cursor-paging",
    "text": "Part Rule limit (query parameter) integer 1–200, default 50 cursor (query parameter) the value of nextCursor from the previous page. Omit it on the first call items (response) the records of this page nextCursor (response) the marker of the next page. Missing on the last page total (response) the total count where the route knows it. Otherwise the field is missing The cursor is stable under insertions: A record that is added while you page does not shift any page. 29 operations of the reference take cursor , 34 take limit . The response, with each scene trimmed to four fields:"
  },
  {
    "id": "api/pagination#filtered-lists-25-and-100",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Filtered lists: 25 and 100",
    "anchor": "filtered-lists-25-and-100",
    "text": "The filtered administration lists (organizations, accounts, devices, roles, grants, keys) cap lower because every row carries more: default 25, at most 100. A limit above that answers 422 Validation.Failed . The reference currently names no bound on limit . The limits that apply are 200/50 for cursor lists and 100/25 for the filtered administration lists (organizations, accounts, devices, roles, grants, keys)."
  },
  {
    "id": "api/pagination#the-exception-page",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "The exception: page",
    "anchor": "the-exception-page",
    "text": "Four lists page with page numbers instead of a cursor, because a pagination bar (“Page 3 of 12”) needs the total count: Route Content GET /v1/devices/{id}/commands commands of a device GET /v1/devices/{id}/events operations log of a device GET /v1/players/{id}/commands commands of a display GET /v1/players/{id}/events operations log of a display Part Rule page integer from 1, default 1 limit 1–200, default 50 Response items , total , page , pageSize , all four required page and pageSize come back in the response even though you sent them: They are the numbers that total refers to."
  },
  {
    "id": "api/pagination#small-collections",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Small collections",
    "anchor": "small-collections",
    "text": "Small collections that cannot be paged answer with a plain list without an items envelope. The response shape is stated at the endpoint in the reference. A client that checks for items recognizes the difference by the structure."
  },
  {
    "id": "api/pagination#collect-all-pages",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Collect all pages",
    "anchor": "collect-all-pages",
    "text": "The loop ends as soon as nextCursor is missing. Do not treat an empty items block as the end, because the last page can be full."
  },
  {
    "id": "api/pagination#command-line",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Command line",
    "anchor": "command-line",
    "text": "Every list command of szenera knows the same three flags: Flag Effect --limit=<count> at most this many rows per page. A value outside the range from 1 to the cap of the route is rejected locally (exit 1) before a call goes out --cursor=<marker> continue reading from this marker. The header line of a result names it: “weiter mit --cursor=…” (continue with) --all follow all pages until no more come. The concatenated output carries no nextCursor How to build a script from this is described under Scripts and CI ."
  },
  {
    "id": "api/pagination#related",
    "path": "/api/pagination",
    "area": "api",
    "areaTitle": "API",
    "page": "pagination",
    "pageTitle": "Paging: limit, cursor, items, nextCursor",
    "section": "Related",
    "anchor": "related",
    "text": "What a response contains Scripts and CI"
  },
  {
    "id": "api/oauth-delegated-access",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Delegated access with OAuth 2.1",
    "anchor": "",
    "text": "By the end of this guide, your application lets a person decide instead of asking for a key: It registers itself, sends the person to give consent, exchanges the code with PKCE for an oat_… access token, and renews or revokes it. It never sees the person’s password."
  },
  {
    "id": "api/oauth-delegated-access#when-this-approach-is-the-right-one",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "When this approach is the right one",
    "anchor": "when-this-approach-is-the-right-one",
    "text": "Situation Choice Your own server talks to Szenera an API key ak_… , which belongs to the organization and is attached to no account Your application runs at other customers’ sites and should see their inventory this approach: A person there consents, and your application gets a token An AI assistant should work on your behalf this approach, usually through the MCP service Delegated access always has two sides: the application that acts and the account on whose behalf it acts. At runtime, the intersection of the two applies. If the person loses a permission, your application loses it at the same moment."
  },
  {
    "id": "api/oauth-delegated-access#the-addresses",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "The addresses",
    "anchor": "the-addresses",
    "text": "Your application has to hard-code exactly one address: the metadata document. Everything else is stated in it, as absolute URLs. Issuer and endpoints will in future be named by the metadata of auth.<your-domain> , if your environment has this host. So do not enter any endpoint by hand, read it from the document you fetch there. The addresses at the API base URL remain reachable until further notice. Route Purpose GET /.well-known/oauth-authorization-server metadata document of the authorization server (RFC 8414) GET /.well-known/oauth-protected-resource metadata document of the protected resource (RFC 9728) POST /oauth/register registration of a public client (RFC 7591) GET /oauth/authorize validates the request and leads to the consent page POST /oauth/token exchange a code or refresh token for a new token POST /oauth/revoke revoke a token (RFC 7009) GET /v1/me/connected-apps the connected applications of your own account DELETE /v1/me/connected-apps/{clientId} take away from an application everything this account gave it The first six addresses carry no /v1 : The standards prescribe their paths literally, and a third-party client looks there and nowhere else. What lies behind the consent lives under /v1 as usual, see Base URL, /v1, and stability ."
  },
  {
    "id": "api/oauth-delegated-access#steps",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Steps",
    "anchor": "steps",
    "text": "Fetch the metadata document. It names the endpoints, the supported methods, and under scopes_supported every permission that delegated access can carry. Register your client, once, without authentication and without a secret. Only public clients exist: token_endpoint_auth_method is always none . Registration is limited to 10 calls per hour and sender address. Store the identifier and do not register anew on every start. Generate a code verifier and its code challenge. The method is exclusively S256 , and there is no plain . Send the person to the browser. They land on the consent page of the portal and see the name of your application, the redirect target, and the requested permissions, bundled into areas with one sentence and one toggle each. When the person returns, check state against the value you sent before you redeem the code. Exchange the code. The body is form-encoded , not JSON. Work with it like with any other Bearer token: Renew in time. The refresh token rotates : After the exchange, the old one is used up, and the response carries a new one. Give back what you no longer need. Revocation always answers 200 , even for an unknown value."
  },
  {
    "id": "api/oauth-delegated-access#options",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Options",
    "anchor": "options",
    "text": ""
  },
  {
    "id": "api/oauth-delegated-access#lifetimes",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Lifetimes",
    "anchor": "lifetimes",
    "text": "Item Valid Access token oat_… one hour Refresh token ort_… thirty days, rotates on every exchange Authorization code ocd_… two minutes, exactly one redemption open consent request ten minutes"
  },
  {
    "id": "api/oauth-delegated-access#permissions",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Permissions",
    "anchor": "permissions",
    "text": "There is no catalog of its own: A scope is a permission key, the same one that an API key may carry as well: scene.read , playlist.write , and so on. The complete list appears as scopes_supported in both metadata documents. GET /v1/permission-catalog also tells you, through the field appliesTo , which permissions are released for machines. Two cuts apply at consent: A permission that machines never carry cannot be granted, and neither can a permission that the consenting person does not hold themselves. Both are listed with their reason in their area. Ask only for what you need: A request that is too broad makes the decision harder and is rejected instead of being silently trimmed."
  },
  {
    "id": "api/oauth-delegated-access#areas-and-partial-consent",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Areas and partial consent",
    "anchor": "areas-and-partial-consent",
    "text": "The consent page does not show the permissions as a list but bundled by the category of the permission catalog: “Scenes”, “Playlists & schedules”, “Media”, “Displays & locations”, “Reports”. Each area carries one sentence about what your application can do in it, one toggle, and the individual permissions in an expandable list. All requested areas are switched on at the start. The same bundling appears later in the Connected applications card, collapsed into a tally (“5 areas, 18 permissions”). Expect a subset. The person may switch individual areas off, and POST /v1/oauth/authorizations/{id}/approve accepts any subset of what was requested. The access token then carries only the granted permissions, and the token response names them under scope : Compare scope with what you asked for instead of assuming it. The platform answers what is missing with 403 at call time, not at the exchange. If you depend on an area, tell the person in plain language and send them through the round trip a second time. There is deliberately no way to extend an existing consent afterward."
  },
  {
    "id": "api/oauth-delegated-access#limits",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Limits",
    "anchor": "limits",
    "text": "GET /oauth/authorize , POST /oauth/token , and POST /oauth/revoke are limited to 60 calls per minute and sender address, the two metadata documents to 120, and registration to 10 per hour. The realtime channel stays closed to delegated access, and you fetch events through Webhooks . The platform operator can block a client. Then no new round trip begins, and a running renewal fails. Existing access tokens run out instead of being revoked. Likewise, an organization can switch delegated access off entirely: The consent then fails with OAuthAuthorization.DelegationDisabled , at the point where the person decides rather than at the exchange."
  },
  {
    "id": "api/oauth-delegated-access#check-the-result",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "GET /v1/me with the token answers 200 and names the account and the organization on whose behalf you act. The token response names under scope the permissions that were actually granted. They can be fewer than those requested. The person sees your application in the portal under Settings › My account › Security in the Connected applications card, expandable by the same areas, and can revoke it there with one click. After a revocation, the same call answers 401 ."
  },
  {
    "id": "api/oauth-delegated-access#if-something-goes-wrong",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "The endpoints of the standard answer in the format of the standard ( { \"error\": …, \"error_description\": … } ) and not in the error format of this API . The routes under /v1 answer as usual. Symptom Meaning Solution 400 invalid_grant on the exchange The code verifier does not match the challenge, or the code has expired. Start the round trip again. The verifier and the challenge must come from the same run. 400 invalid_grant , and all tokens are gone A code or a refresh token was redeemed a second time. The whole token family expires: Two holders of the same secret cannot be told apart. Discard the old token, have the person consent again, and never run two processes on the same token. 400 without a redirect redirect_uri is not in the list from the registration. This very address is what could not be verified. Enter the address at registration and send it word for word at the exchange. 410 OAuthAuthorization.Expired when opening the consent page The request is older than ten minutes. Explicitly not “not found”. Send the person there again. 422 naming permission keys A scope was confirmed that the person cannot hold or that is not intended for applications. Remove the named key from the request. 401 during operation The access token is older than one hour, or the person has revoked it. Renew once. If that fails too, have the person consent again. The OAuthClient.* and OAuthAuthorization.* codes are listed in full, with meaning, trigger, and status, under Error codes: accounts a"
  },
  {
    "id": "api/oauth-delegated-access#related",
    "path": "/api/oauth-delegated-access",
    "area": "api",
    "areaTitle": "API",
    "page": "oauth-delegated-access",
    "pageTitle": "Delegated access with OAuth 2.1",
    "section": "Related",
    "anchor": "related",
    "text": "Authentication: keys and sessions The MCP service for AI assistants Handbook: Allow and revoke connected applications"
  },
  {
    "id": "api/scim-provisioning",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "Provisioning with SCIM 2.0",
    "anchor": "",
    "text": "By the end of this page, a directory service connects the accounts of an organization to Szenera: Joining creates an account, leaving suspends it, and a group change sets the role. You learn what the token looks like, which endpoints exist, what is mapped to what, and what happens on conflicts."
  },
  {
    "id": "api/scim-provisioning#the-scim-token",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "The SCIM token",
    "anchor": "the-scim-token",
    "text": "The interface lives under /scim/v2 and not under /v1 : The path belongs to a third-party protocol, and a directory service validates its own data against the address it talks to. What Value Base URL $SZENERA_API/scim/v2 Credential Authorization: Bearer sct_… Content type application/scim+json in both directions Reach exactly one organization per token If your environment has a sign-in host, the portal will in future name https://auth.<your-domain>/scim/v2 as the base URL, and exactly this is what appears in meta.location of the responses. New setups enter it from the start, and an existing one at the API base URL remains reachable until further notice. The token is created in the portal under Settings › Organization › Company sign-in , section Provisioning (SCIM) . Its value appears there exactly once. It is accepted on no other route of the platform: Everywhere else it is rejected with 403 , and conversely no API key reaches /scim/v2 . The sign-in method is a condition, not a recommendation. An account created this way gets no password and signs in through the organization’s provider. Without an active provider, no token can be issued."
  },
  {
    "id": "api/scim-provisioning#the-endpoints",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "The endpoints",
    "anchor": "the-endpoints",
    "text": "Route Methods /scim/v2/ServiceProviderConfig GET , feature set: partial update yes, filter yes, bulk no, sorting no /scim/v2/ResourceTypes , /scim/v2/ResourceTypes/{id} GET /scim/v2/Schemas , /scim/v2/Schemas/{id} GET /scim/v2/Users GET (list and filter), POST /scim/v2/Users/{id} GET , PUT , PATCH , DELETE /scim/v2/Groups GET (list and filter), POST /scim/v2/Groups/{id} GET , PUT , PATCH , DELETE (rejected, see below) A directory service starts with the first row: Create an account. This is repeatable, and the same address returns the same account: Suspend an account. A partial update is the standard way:"
  },
  {
    "id": "api/scim-provisioning#paging-and-filtering",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "Paging and filtering",
    "anchor": "paging-and-filtering",
    "text": "startIndex counts from one , and count is capped at 200. Exactly the form attribute eq \"value\" is accepted as a filter: userName eq and externalId eq on Users , displayName eq on Groups . Everything else is 400 with scimType: \"invalidFilter\" , because a silently ignored filter would return the entire set of accounts."
  },
  {
    "id": "api/scim-provisioning#the-mapping",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "The mapping",
    "anchor": "the-mapping",
    "text": "SCIM Szenera userName the email address. It is unique per organization and therefore the shared key externalId is stored, is unique per organization, and can be found via externalId eq name.givenName , name.familyName , displayName the display name of the profile active: false suspend the account. active: true reactivates it DELETE /Users/{id} suspends , does not delete Group a role of the organization. displayName is its name members the accounts that hold this role Fields that Szenera does not keep ( title , phoneNumbers , extension schemas) are silently discarded. Your mapping in the directory service does not need to be thinned out for that. Four peculiarities that every integration must know about: Nothing is ever deleted. Audit log entries, uploaded media, and created programs hang off an account. Permanent deletion remains a deliberate step by a person in the portal. An account holds exactly one role, never none. An add assigns the role and replaces the previous one. A remove resets to the base role. A PUT is both together, because anyone who is missing from the list falls back. Groups are not created through SCIM. The substance of a role is its permissions, and the protocol does not carry them. POST /Groups therefore matches an existing role by its displayName . An unknown name gets 400 with scimType: \"invalidValue\" . Create the roles in the portal first and name the groups in the directory identically. Every change is repeatable. A directory service retries after eve"
  },
  {
    "id": "api/scim-provisioning#errors",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "Errors",
    "anchor": "errors",
    "text": "Under /scim/v2 , the error format of the protocol applies, not that of this API : A directory service evaluates scimType programmatically and decides accordingly whether to retry, skip the entry, or stop. status is a string, as the protocol prescribes. Case Response The credential is missing or invalid 401 The credential is something other than sct_… 403 The address already exists in this organization 409 with scimType: \"uniqueness\" The filter has a form other than attribute eq \"value\" 400 with scimType: \"invalidFilter\" POST /Groups names a role that does not exist 400 with scimType: \"invalidValue\" DELETE /Groups/{id} 400 with scimType: \"mutability\" . Roles are deleted in the portal"
  },
  {
    "id": "api/scim-provisioning#related",
    "path": "/api/scim-provisioning",
    "area": "api",
    "areaTitle": "API",
    "page": "scim-provisioning",
    "pageTitle": "Provisioning with SCIM 2.0",
    "section": "Related",
    "anchor": "related",
    "text": "Authentication: keys and sessions Base URL, /v1, and stability Handbook: Accounts from your employee directory (SCIM)"
  },
  {
    "id": "api/mcp-service",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "The MCP service for AI assistants",
    "anchor": "",
    "text": "The MCP service gives an AI assistant a curated selection of tools for your inventory: viewing scenes and media, checking displays and locations, proposing drafts, and, after explicit confirmation, setting a program. This page lists the address, the authentication, the tools, the limits, and the errors."
  },
  {
    "id": "api/mcp-service#what-the-service-is",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "What the service is",
    "anchor": "what-the-service-is",
    "text": "It is a thin adapter in front of the same API that the portal and the command line use. It holds no data, knows no secret, and does not validate a credential: Your Bearer token travels unchanged on to the platform, and permission resolution remains that of the API. There is no route, field, or rule there that exists only there. What it contributes is four things: a selection instead of a mapping of the whole API, a confirmation before every effect, limits for result size and runtime, and the guarantee that a tool deletes only one thing: a display that runs in the browser. The service speaks English. Tool names, input fields, response fields, status values, error kinds, descriptions, and hints are English, so that an assistant reads the same names in any language. This page quotes the responses of the service verbatim."
  },
  {
    "id": "api/mcp-service#connect",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "Connect",
    "anchor": "connect",
    "text": "What Value Transport Streamable HTTP per the MCP specification Address https://mcp.<your-domain>/mcp Authentication Authorization: Bearer oat_… or Authorization: Bearer ak_… Resource metadata GET https://mcp.<your-domain>/.well-known/oauth-protected-resource (RFC 9728) Health checks GET /health/live , GET /health/ready An assistant that supports OAuth needs only the address: Without a credential, /mcp answers 401 with the WWW-Authenticate header, which points to the resource metadata. From there, the client finds the authorization server and starts the round trip described in Delegated access with OAuth 2.1 . A tool without this path enters an API key instead. Right at the handshake, the service checks the credential once against GET /v1/me . If it is rejected, the service answers 401 with the same challenge. The client then starts authentication instead of ending up in a conversation in which every tool fails. The credential belongs on every request, even within a session. That applies equally to GET /mcp , which opens the event stream of a session, and to DELETE /mcp , which ends it. The service checks both against GET /v1/me on every request before it even looks at the session. Without a credential, they answer 401 with error=\"invalid_request\" , and with a rejected credential 401 with error=\"invalid_token\" . The session identifier alone therefore opens nothing. The session that the handshake opens lives for 24 hours, sliding : Every call resets the deadline. Anyone who exc"
  },
  {
    "id": "api/mcp-service#the-permissions-the-client-asks-for",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "The permissions the client asks for",
    "anchor": "the-permissions-the-client-asks-for",
    "text": "The resource metadata lists under scopes_supported the permissions that the tools of this service need, exactly those and no others. The same list appears as scope in the WWW-Authenticate header, so that a client that does not fetch the metadata has it too. A client passes them on unchanged into its authentication. Without scope , the authorization server rejects the request. On the consent page, these permissions then appear bundled into areas (“Scenes”, “Playlists & schedules”, “Media”, “Displays & locations”, “Reports”), each with one sentence and one toggle. All requested areas are switched on at the start, and a person may switch individual ones off: The consent may be a subset. Your access token then carries only the granted permissions, and the token response names them under scope . If a tool lacks a permission, it answers permission_denied and names the area that the consent would have to include, in the words of the consent page, here in the English version (“Displays & locations”, where a German consent page says “Displays und Standorte”): With that, an assistant can tell its human which toggle to switch on in the next round trip. A consent cannot be extended afterward. The way is to revoke it and allow it again."
  },
  {
    "id": "api/mcp-service#new-permissions-new-consent",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "New permissions, new consent",
    "anchor": "new-permissions-new-consent",
    "text": "If a tool is added that needs a permission that no tool needed before, this list grows, and an existing access token does not grow with it . It carries the permissions that were granted at the time of its consent. The new ones are not included, and the tools behind them answer permission_denied . whoami tells you this before anyone tries it out: It compares the permissions of your credential with those that the tools of this service need and names the missing ones under missingPermissions . The second sentence is no ornament: Delegated access never gets more than the account behind it has. If a permission is still in the list after allowing again, it is missing from the role of the account, and then only someone who assigns roles can help. With an API key, you instead issue a new one with the matching permission. The permission catalog for delegated access includes all permissions that this service names, including those for displays and analytics. Every tool is therefore reachable in both ways: through delegated access and through an API key that carries the same permissions. What a tool changes remains bound to your explicit confirmation anyway (see below)."
  },
  {
    "id": "api/mcp-service#the-tools",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "The tools",
    "anchor": "the-tools",
    "text": "The tools fall into three risk classes, and a client shows them in this order. Class Tools What they do Read whoami , list_scenes , get_scene , get_scene_document , list_scene_templates , list_widgets , get_widget_schema , list_graphics , list_assets , list_players , get_player , get_player_screenshot , list_display_groups , list_locations , get_location , list_playlists , get_playlist , list_schedules , get_hygiene , validate_scene View the inventory: who the credential is, which scenes, widgets, graphics, media, displays, video walls, locations, playlists, and programs exist, what is in a scene, what can be seen on a display (including its most recent capture as an image), what is left lying around, and whether a scene document follows the rules Propose create_scene_from_template , create_scene , update_scene , render_scene , create_playlist , add_to_playlist Create and change drafts that appear on no display as long as nobody publishes them, and view a published version as an image Write publish_scene , set_player_program , set_display_group_program , set_player_override , clear_player_override , send_player_command , assign_player_location , create_location , set_location_opening_hours , create_web_display , issue_web_display_link , delete_web_display Publish, set a program, override and release again, issue a remote maintenance command, assign a display to a location, create a location, set its opening hours, and create a display that runs in the browser, give it a new c"
  },
  {
    "id": "api/mcp-service#a-display-that-shows-something",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "A display that shows something",
    "anchor": "a-display-that-shows-something",
    "text": "create_web_display is the way an assistant can show anything at all. Without it, every other tool presupposes a display that someone has connected at some point. It answers with the identifier of the display and a connection URL . The URL belongs in the answer to the person: Opening it in a browser on the target device is the one step of this process that no assistant can do itself. Until that happens, the display appears in the fleet as “Not connected” ( \"status\": \"not_connected\" in the response), with its name, its location, and, if desired, its program already set. It occupies a booked seat from the moment it is created. If all seats are taken, the service answers no_seat_available instead of a technical error. As soon as a browser has opened the URL, the display shows every further change by itself, without needing a reload. issue_web_display_link gives the same display a new URL if the browser behind it has lost its storage. Whoever opens it takes over the display, and the browser that showed it until then loses it. Name, location, program, and the booked seat remain untouched. The cleanup questions are not tools of their own but filters on the lists that exist anyway: unused media, published scenes without an assignment, displays without a program or without a check-in for a long time. get_hygiene counts them and names for each number the tool that lets you view the rows behind it. Two tools of the Propose class carry confirm nonetheless: create_scene and update_scene ,"
  },
  {
    "id": "api/mcp-service#build-a-scene",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "Build a scene",
    "anchor": "build-a-scene",
    "text": "The way from idea to image is a chain of six steps. Every tool description names the next one, so an assistant does not have to guess the order. 1. View the widgets. list_widgets names the widgets a scene is made of: the core widgets text , image , video , shape , graphic , and document , which are always there, and the widgets of the apps that your organization has installed. Each entry shows the identifier, app, name, a sentence, the required fields, and whether the app is installed. A widget of an app that is not installed can be written into a document but runs on no display. If your organization has booked no app, all app widgets show installed: false , and the response also says so in plain text instead of leaving you to guess. 2. Get the rules. get_widget_schema returns, for a widget, the complete JSON Schema document of its configuration (Draft 2020-12): every field with title and description , the required fields under required , and under examples at least one complete example configuration. Take an example and swap the values: That is the shortest guide there is. The initial configuration and the preview variants of the widget are included as well. One field the schema does not name with values: the graphicId of the core widget graphic . It is a fixed identifier from the graphics library, and list_graphics lists it, with category and keywords, searchable by an intent such as “pfeil” (arrow) or “kaffee” (coffee), because the search terms are German words. It is not "
  },
  {
    "id": "api/mcp-service#confirmation",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "Confirmation",
    "anchor": "confirmation",
    "text": "Every writing tool carries the field confirm . Without confirm: true, nothing happens: The tool reads the current state and answers with a preview: which targets it would affect, what they are called, whether they are online, what would be replaced. Only the second call with the same values and confirm: true executes and answers with \"status\": \"executed\" , \"executed\": true , and the result under result . The preview is explicitly not a reservation: It holds nothing. If something changes between the two calls, the concurrency check of the API applies, not the service. In addition, all tools carry the hints readOnlyHint and destructiveHint , so that a client can ask its own follow-up question. Every writing call travels with an Idempotency-Key that is derived from session, tool name, and arguments: The same intent takes effect once, even if it arrives twice."
  },
  {
    "id": "api/mcp-service#limits",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "Limits",
    "anchor": "limits",
    "text": "Limit Default Runtime of a tool call 30 seconds Runtime of a single call to the API 15 seconds Length of a result 20,000 characters, anything beyond that is visibly truncated Entries in a list 50 per response. The number also caps the requested page size Idle session 24 hours, sliding. Every call resets the deadline The operator of your environment can set these values differently. Results contain no signed download URLs, and names and texts from a response are data for the assistant, not instructions. Every tool description says so explicitly."
  },
  {
    "id": "api/mcp-service#if-something-goes-wrong",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "A failed tool answers with three fields: error (one of the kinds below), hint (one sentence in English), and correlationId (the identifier with which the operations team can find the call again). The field paths and identifiers from the error body of the API are deliberately retained. error Meaning Solution unauthenticated The platform rejected the credential ( 401 ). Sign in again. An access token oat_… is valid for one hour. permission_denied The credential lacks the permission ( 403 ). Include the permission in the consent, or issue the key with the matching permission. not_found There is nothing for this identifier in this organization ( 404 ). Take the identifier from a list fetched with the same credential. conflict The object has changed since it was read ( 409 ). Read again and decide again. invalid_input The platform rejected the values ( 422 ). Check the values. no_seat_available All booked seats of the organization are occupied ( 402 ). A person books an additional seat in the portal. rate_limited The rate limit is exhausted ( 429 ). Wait, see Throttling and rate-limit headers . platform_unreachable The API does not answer. Try again later. The service can do nothing without it. timeout The allotted time has run out. Ask a narrower question, for example with a filter or a smaller list. canceled The call was canceled, for example because the connection dropped. Call again. unexpected The platform answered in a way that the service cannot classify. Try again later an"
  },
  {
    "id": "api/mcp-service#related",
    "path": "/api/mcp-service",
    "area": "api",
    "areaTitle": "API",
    "page": "mcp-service",
    "pageTitle": "The MCP service for AI assistants",
    "section": "Related",
    "anchor": "related",
    "text": "Delegated access with OAuth 2.1 Authentication: keys and sessions Idempotency from the start"
  },
  {
    "id": "api/locations",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "Locations",
    "anchor": "",
    "text": "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."
  },
  {
    "id": "api/locations#what-a-location-is",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "What a location is",
    "anchor": "what-a-location-is",
    "text": "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 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 The time zone belongs to the location, not to the display: Two displays in the same room never have two different ones."
  },
  {
    "id": "api/locations#the-routes",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "The routes",
    "anchor": "the-routes",
    "text": "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"
  },
  {
    "id": "api/locations#assign-a-display",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "Assign a display",
    "anchor": "assign-a-display",
    "text": "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. \"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."
  },
  {
    "id": "api/locations#opening-hours",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "Opening hours",
    "anchor": "opening-hours",
    "text": "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. 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 o"
  },
  {
    "id": "api/locations#where-else-the-location-appears",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "Where else the location appears",
    "anchor": "where-else-the-location-appears",
    "text": "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 Assistant the tools list_locations , get_location , assign_player_location , create_location , and set_location_opening_hours of the MCP service Apps the location of the display in the resolver context, with location-hours including opening hours, see Data access, permissions, and terms"
  },
  {
    "id": "api/locations#if-something-goes-wrong",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "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 ."
  },
  {
    "id": "api/locations#related",
    "path": "/api/locations",
    "area": "api",
    "areaTitle": "API",
    "page": "locations",
    "pageTitle": "Locations",
    "section": "Related",
    "anchor": "related",
    "text": "Concurrent changes: ETag and If-Match Commands for your organization Handbook: Locations"
  },
  {
    "id": "api/web-displays",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "Web displays",
    "anchor": "",
    "text": "A web display is a display with no device behind it, only a browser tab. This page lists the routes, the way from creation to the first picture, the two states it can live in, and the limits that distinguish it from a device."
  },
  {
    "id": "api/web-displays#what-a-web-display-is",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "What a web display is",
    "anchor": "what-a-web-display-is",
    "text": "A full-fledged display of the fleet: It has an identifier ply_… , a name, a location, a program, and a booked seat. What it lacks is the machine: It runs in a browser page, and its whole identity is a secret in the storage of that browser. This leads to the peculiarity of this approach: Creating and connecting are two steps, and no software can do the second. The platform creates the display and issues an address, and a person must open this address in a browser on the target device. Until then, the display is in the state paired-not-claimed , which the interface shows as “Not connected”. State Meaning paired-not-claimed created, but no browser has opened the address yet. Already occupies a booked seat. online / stale / offline a browser is attached to it. As with any display, the state comes from the age of the last check-in"
  },
  {
    "id": "api/web-displays#the-routes",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "The routes",
    "anchor": "the-routes",
    "text": "Route Permission Note POST /v1/web-displays fleet.web-display.manage or fleet.player.pair 201 . Creates the display and the connection URL in one step POST /v1/web-displays/{id}/link fleet.web-display.manage or fleet.player.pair 201 . A new URL for an existing web display POST /v1/web-displays/batch fleet.web-display.manage or fleet.player.pair 201 . Creates up to 50 displays in one step ( count , optionally locationId and namePrefix ), with numbered names and one connection URL each. All or nothing: if the booked seats do not suffice, none is created. The response has the shape of a provisioning file, and each address works once and expires after 24 hours DELETE /v1/web-displays/{id} fleet.web-display.manage removes the display and the browser access from the inventory GET /v1/web-displays/address fleet.web-display.manage or fleet.player.pair the address of the display software, for panels onto which no address can be copied All routes carry x-stability: beta , including POST /v1/web-displays/batch . Their shape can still move, and the flow itself has already shifted once in this wave (earlier, the display was created only when the link was opened). For deletion, fleet.player.pair is not enough here. Whoever holds the pairing permission has been given a permission to connect and none to remove. Silently giving them one here would turn an old consent into a new one."
  },
  {
    "id": "api/web-displays#create-and-connect",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "Create and connect",
    "anchor": "create-and-connect",
    "text": "connectUrl is the address for the person at the target device. It starts with the address of the display software, which GET /v1/web-displays/address also names, and it is valid once : As soon as a browser has opened it, the token is used up. The deadline depends on whether a browser is already attached to this display: 24 hours for the first address, two hours after that. An initial program is deliberately not provided on this route. The assignment has its own route with its own checks, so a display with content is created in two calls: The order does not matter: A display that was assigned a program before it was connected shows it as soon as the browser is there."
  },
  {
    "id": "api/web-displays#a-new-address",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "A new address",
    "anchor": "a-new-address",
    "text": "The reason is always the same: The browser no longer knows that it is this display (cleared storage, a fresh profile, a different machine), or the first address expired without anyone opening it. Whoever opens the new address takes over the display. The browser that showed it until then loses it, together with its running renderer sessions, and all older open addresses of the same display expire. Name, location, program, history, and the booked seat remain untouched. That is exactly where this differs from creating a new display."
  },
  {
    "id": "api/web-displays#delete",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "Delete",
    "anchor": "delete",
    "text": "Both halves disappear: the display in the fleet and the browser device behind it. The booked seat becomes free, and program, override, and history are dissolved. A display with a device is rejected at this route. For that there is DELETE /v1/players/{id} , which remains reserved for a person in a session."
  },
  {
    "id": "api/web-displays#what-a-web-display-cannot-do",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "What a web display cannot do",
    "anchor": "what-a-web-display-cannot-do",
    "text": "No machine stands behind the surface that could be controlled. That is not a gap in the interface but the nature of the thing: Not possible Why System image, channel, fallback layer Whoever looks after the computer maintains the browser and the operating system Switching the panel on and off, panel brightness CEC and DDC/CI address the machine at the cable, not a page in a browser Restart of the machine The tab is reloaded, and the computer stays untouched Screenshot without an extension No web standard allows a page to photograph itself Two panels on one browser A second one needs a second browser profile and becomes a display of its own with its own seat"
  },
  {
    "id": "api/web-displays#if-something-goes-wrong",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution 409 WebDisplay.NotABrowserDisplay The identifier belongs to a display with a device. DELETE /v1/players/{id} or POST /v1/devices/{id}/reconnect-link , both for people in a session. 402 Billing.LicensesExhausted All booked seats are occupied. The response names booked and used . Book an additional seat or delete a display that is no longer needed. 403 Billing.FeatureNotEntitled The Displays without an agent capability is switched off for this organization. Have the platform operator enable it. 404 Location.NotFound when creating The named location identifier does not belong to any location of this organization. Take the identifier from GET /v1/locations . The address leads to a pairing code The token was used up or expired, and the browser then registered as a new device. Issue a new address and open that one. The empty device stays unpaired and expires."
  },
  {
    "id": "api/web-displays#related",
    "path": "/api/web-displays",
    "area": "api",
    "areaTitle": "API",
    "page": "web-displays",
    "pageTitle": "Web displays",
    "section": "Related",
    "anchor": "related",
    "text": "The MCP service : the tools create_web_display , issue_web_display_link , and delete_web_display Locations Error codes: devices, playback, notifications, operations"
  },
  {
    "id": "api/error-codes-general",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Error codes: possible on any route",
    "anchor": "",
    "text": "These codes arise in the platform layer, not in a domain area, so they can occur on any route under /v1 . Their areas are authentication, permissions, input validation, conflicts, throttling, payload, HTTP, and the operator routes for events, cache, audit log, and background jobs."
  },
  {
    "id": "api/error-codes-general#how-to-read-the-tables",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Read the tables like this: Code is the value of the code field in the error body. Your logic switches on it, never on message . Meaning and Typical trigger classify the case. HTTP is the status actually sent. Expected reaction is your client’s response, drawn from a fixed vocabulary that is the same on all seven error-code pages: “check credentials” (401), “check permission or package” (402, 403), “correct the input” (400, 413, 415, 422), “reload, resend” (version conflicts), “do not retry, check the operation” (404, 409, 410, 426), “wait (Retry-After)” (429), and “retry later, report the trace ID if it recurs” (5xx)."
  },
  {
    "id": "api/error-codes-general#the-codes",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-general#authentication-auth",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Authentication (Auth)",
    "anchor": "authentication-auth",
    "text": "The credential is missing, invalid, or expired. Almost all codes answer 401. Only a disallowed impersonation is a 403, and remote maintenance with a protocol version the API does not serve is a 426. For integrations, Auth.ApiKeyRevoked , Auth.ApiKeyExpired , and Auth.ApiKeyUnknown are the three you are most likely to see. Code Meaning Typical trigger HTTP Expected reaction Auth.Required Authentication required Request without a valid token 401 check credentials Auth.TokenExpired Access token expired Lifetime of 15 minutes exceeded 401 check credentials Auth.TokenInvalid Token invalid Tampered or unreadable token 401 check credentials Auth.TokenSignatureInvalid Token signature invalid Wrong signing key 401 check credentials Auth.TokenKidUnknown Signing key ID unknown Token with a changed key ID 401 check credentials Auth.TokenMalformed Token structurally malformed Not a valid access token 401 check credentials Auth.SessionRevoked Session revoked Sign-out or ending of the session 401 check credentials Auth.MfaRequired Second factor required Sign-in without a second factor 401 check credentials Auth.MfaPending Second-factor step pending Intermediate step before the second factor is confirmed 401 check credentials Auth.MfaCodeInvalid Second-factor code incorrect Wrong one-time or recovery code 401 check credentials Auth.ImpersonationNotPermitted Impersonation not permitted Disallowed attempt to take over another session 403 check permission or package Auth.ApiKeyRevoked API key r"
  },
  {
    "id": "api/error-codes-general#permissions-permission",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Permissions (Permission)",
    "anchor": "permissions-permission",
    "text": "The credential is valid, but the permission is missing. All codes answer 403. You get Permission.OperatorOnly when a key or session of an organization calls a route under /v1/platform/… , deliberately not a 401, so that a process does not loop on sign-in. Code Meaning Typical trigger HTTP Expected reaction Permission.Denied Permission missing The required permission is missing 403 check permission or package Permission.OperatorOnly Route for operator accounts only Session or key of an organization on a /v1/platform/… route. A code of its own instead of Auth.Required , because a process that gets a 401 repeats the sign-in and goes in circles. A new credential does not help here, only a different route does 403 check permission or package Permission.UnknownKey Unknown permission key Not in the catalog 403 check permission or package Permission.OwnershipDenied Object access denied No grant for the resource 403 check permission or package Permission.ResourceIdMissing Resource ID missing Object-bound check without an ID 403 check permission or package"
  },
  {
    "id": "api/error-codes-general#input-validation-validation",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Input validation (Validation)",
    "anchor": "input-validation-validation",
    "text": "Body, parameters, or headers violate the schema or a rule. The usual status is 422. Only a missing idempotency header on a route that enforces it answers 409. Code Meaning Typical trigger HTTP Expected reaction Validation.Failed Input invalid Body or parameters violate the schema 422 correct the input Validation.IdempotencyKeyRequired Idempotency-Key required Mutation without Idempotency-Key 409 do not retry, check the operation Validation.IdempotencyKeyReused Idempotency-Key with a different body The same key for a second operation with different content 422 correct the input Validation.UrlProtocolBlocked URL protocol blocked Disallowed scheme (e.g., file: ) 422 correct the input Validation.UrlProtocolNotAllowed URL protocol not allowed Protocol outside the allowlist 422 correct the input Validation.FileTypeUnsupported File type not supported Upload of a disallowed format 422 correct the input Validation.PathTraversalBlocked Path traversal blocked ../ in a path parameter 422 correct the input"
  },
  {
    "id": "api/error-codes-general#conflicts-conflict",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Conflicts (Conflict)",
    "anchor": "conflicts-conflict",
    "text": "The state of the resource does not allow the action, or someone else wrote at the same time. All codes answer 409. Conflict.OptimisticLock is the case described in Concurrent changes . Code Meaning Typical trigger HTTP Expected reaction Conflict.OptimisticLock Version conflict If-Match / expectedVersion outdated 409 reload, resend Conflict.DuplicateKey Uniqueness violated Duplicate value in a unique field 409 do not retry, check the operation Conflict.ReferenceConstraint Reference constraint violated Reference to a record that does not exist 409 do not retry, check the operation Conflict.ResourceLocked Resource locked Concurrent access 409 reload, resend"
  },
  {
    "id": "api/error-codes-general#throttling-ratelimit",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Throttling (RateLimit)",
    "anchor": "throttling-ratelimit",
    "text": "Too many requests in the window. All codes answer 429 with Retry-After . Which routes are throttled is described under Throttling . Code Meaning Typical trigger HTTP Expected reaction RateLimit.Exceeded Rate limit exceeded Too many requests 429 wait (Retry-After) RateLimit.GlobalCircuitBreaker Global circuit breaker open Platform-wide overload protection 429 wait (Retry-After)"
  },
  {
    "id": "api/error-codes-general#payload",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Payload",
    "anchor": "payload",
    "text": "The body is too large or uses an encoding the server does not process. All codes answer 413. Code Meaning Typical trigger HTTP Expected reaction Payload.TooLarge Body too large Limit exceeded 413 correct the input Payload.UnsupportedEncoding Encoding not supported Content-Encoding that cannot be processed 413 correct the input"
  },
  {
    "id": "api/error-codes-general#origin-and-http-cors-http",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Origin and HTTP (Cors, Http)",
    "anchor": "origin-and-http-cors-http",
    "text": "Rejection of a browser origin and the general HTTP errors: wrong method, wrong media type, unreadable request. Code Meaning Typical trigger HTTP Expected reaction Cors.OriginRejected Origin not allowed Request from a disallowed origin 403 check permission or package Http.BadRequest Malformed request Request that the server cannot read 400 correct the input Http.MethodNotAllowed Method not allowed Wrong HTTP verb 405 correct the input Http.UnsupportedMediaType Media type not supported Wrong Content-Type 415 correct the input"
  },
  {
    "id": "api/error-codes-general#events",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Events",
    "anchor": "events",
    "text": "Routes of platform operations for redelivering events. You cannot reach them with an organization’s key. Code Meaning Typical trigger HTTP Expected reaction Events.EventNotFound Event not found Redelivery of an unknown event 404 do not retry, check the operation Events.EventNotReplayable Event not replayable Redelivery of an event that cannot be replayed 409 do not retry, check the operation Events.BulkReplayLimitExceeded Bulk replay limit exceeded Too many events at once 422 correct the input"
  },
  {
    "id": "api/error-codes-general#cache",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Cache",
    "anchor": "cache",
    "text": "Routes of platform operations for discarding cache entries. Code Meaning Typical trigger HTTP Expected reaction Cache.PatternMatchingInvalid Invalid cache pattern Malformed pattern for discarding 400 correct the input Cache.OpsInvalidateReasonTooShort Reason too short Discarding without a sufficient reason 400 correct the input"
  },
  {
    "id": "api/error-codes-general#platform-audit-log-audit",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Platform audit log (Audit)",
    "anchor": "platform-audit-log-audit",
    "text": "Routes of platform operations for rotating the pseudonymization key and resolving pseudonyms. Code Meaning Typical trigger HTTP Expected reaction Audit.RotationCooldownActive Key rotation cooldown active Rotating the pseudonymization key again too soon 409 do not retry, check the operation Audit.LookupRateLimitExceeded Lookup rate exceeded Too many pseudonym lookups 429 wait (Retry-After)"
  },
  {
    "id": "api/error-codes-general#background-jobs-jobs",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Background jobs (Jobs)",
    "anchor": "background-jobs-jobs",
    "text": "Routes of platform operations for queues and permanently failed jobs. Code Meaning Typical trigger HTTP Expected reaction Jobs.JobNotFound Job not found Action on an unknown job 404 do not retry, check the operation Jobs.JobNotKillable Job cannot be aborted Forced abort not possible 409 do not retry, check the operation Jobs.DeadLetterNotFound Entry for permanently failed jobs missing Re-queuing an unknown entry 404 do not retry, check the operation Jobs.ReplayAlreadyDone Re-queuing already done Duplicate re-queuing 409 do not retry, check the operation Jobs.BulkReplayLimitExceeded Bulk replay limit exceeded Too many jobs at once 422 correct the input"
  },
  {
    "id": "api/error-codes-general#resources-resource",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Resources (Resource)",
    "anchor": "resources-resource",
    "text": "The general fallback when no route matches the path. Code Meaning Typical trigger HTTP Expected reaction Resource.NotFound Resource not found Generic 404 fallback 404 do not retry, check the operation"
  },
  {
    "id": "api/error-codes-general#count",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 66 codes . Together with the other six error-code pages this gives 493 domain codes (4xx) and 119 server errors (5xx), 612 codes in all — the complete catalog of the platform. Codes are counted by the kind of error and not by the status sent: the 493 codes your client can react to specifically count as domain errors (nine of them still answer with a 5xx, because a downstream service fails, the platform is briefly not ready to calculate, or it lacks access), and the 119 for which only retrying or reporting remains count as server errors. Counted by status alone, the figures would be 484 and 128."
  },
  {
    "id": "api/error-codes-general#related",
    "path": "/api/error-codes-general",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-general",
    "pageTitle": "Error codes: possible on any route",
    "section": "Related",
    "anchor": "related",
    "text": "The error format When the first call fails"
  },
  {
    "id": "api/error-codes-accounts",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "Error codes: accounts and sessions",
    "anchor": "",
    "text": "The codes of the Identity and Sessions areas: user accounts, operator accounts, API keys, connected applications, SCIM tokens, device accounts, sign-in, second factor, refresh, and impersonation."
  },
  {
    "id": "api/error-codes-accounts#how-to-read-the-tables",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Read the tables like this: Code is the value of the code field in the error body. Your logic switches on it, never on message . Meaning and Typical trigger classify the case. HTTP is the status actually sent. Expected reaction is your client’s response, drawn from a fixed vocabulary that is the same on all seven error-code pages: “check credentials” (401), “check permission or package” (402, 403), “correct the input” (400, 413, 415, 422), “reload, resend” (version conflicts), “do not retry, check the operation” (404, 409, 410, 426), “wait (Retry-After)” (429), and “retry later, report the trace ID if it recurs” (5xx)."
  },
  {
    "id": "api/error-codes-accounts#the-codes",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-accounts#accounts-keys-devices-identity",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "Accounts, keys, devices (Identity)",
    "anchor": "accounts-keys-devices-identity",
    "text": "Accounts, operator accounts, API keys, and devices. For integrations, the key codes are the most important. ApiKey.ScopeEscalationBlocked (403) means the key asks for more than its creator is allowed to do. ApiKey.ScopeNotKeyCapable (422) means a chosen permission is not intended for keys, and asking does not help here, only a different approach. Two more are ApiKey.ExpiresAtInPast (422) and ApiKey.AlreadyRevoked (409), because revocation is final. The OperatorApiKey.* codes concern operator keys ( opk_ ). Then there are the forms of access that are not tied to a person: OAuthClient.* and OAuthAuthorization.* for connected applications . OAuthAuthorization.Expired (410) and OAuthAuthorization.AlreadyDecided (409) are the ones you will see most often, and both mean “restart the consent”. Finally, ScimToken.* covers the access with which a staff directory syncs accounts . Among the Device.* codes, Device.Blocked (409) rejects a blocked device until the operator lifts the block. Code Meaning Typical trigger HTTP Expected reaction User.NotFound Account not found Access to an unknown account ID 404 do not retry, check the operation User.EmailAlreadyExists Email already taken Creation with an address that is already in use 409 do not retry, check the operation User.PlanSeatQuotaExceeded Seat limit reached Account over the seat limit of the package 422 correct the input User.MissingCredential Sign-in credentials missing Account with neither a password nor a company sign-in 422 corre"
  },
  {
    "id": "api/error-codes-accounts#sign-in-and-sessions-sessions",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "Sign-in and sessions (Sessions)",
    "anchor": "sign-in-and-sessions-sessions",
    "text": "Sign-in with a password, magic link, company sign-in, and platform-wide providers, second factor, refresh, password reset, and impersonation by the operator. The codes with 410 mark links that are used up or expired. A new link is the only way. Code Meaning Typical trigger HTTP Expected reaction Login.InvalidCredentials Sign-in credentials incorrect Wrong password or wrong email address 401 check credentials Login.AccountSuspended Account suspended Sign-in of a suspended account 403 check permission or package Login.TenantSuspended Organization suspended Sign-in to a suspended organization 403 check permission or package Login.ChoiceTicketInvalid Choice ticket invalid Organization choice with an unknown, expired, or already redeemed ticket (also: another device) 401 check credentials Login.OrganizationNotEligible Organization not selectable The chosen organization is not in the ticket’s set 403 check permission or package Login.SsoRequired Company sign-in enforced Password, magic link, or reset for an address in a verified domain of the company sign-in 403 check permission or package Login.HandoffTicketInvalid Handoff ticket invalid Ticket unknown, expired (3 min), or already redeemed 401 check credentials Mfa.InvalidCode Second-factor code invalid Wrong code at sign-in 401 check credentials Mfa.TooManyAttempts Too many second-factor attempts Failed-attempt limit exceeded 429 wait (Retry-After) Mfa.NotEnabled Second factor not active Sign-in with a second factor although none"
  },
  {
    "id": "api/error-codes-accounts#count",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 103 codes . Together with the other six error-code pages this gives 493 domain codes (4xx) and 119 server errors (5xx), 612 codes in all — the complete catalog of the platform."
  },
  {
    "id": "api/error-codes-accounts#related",
    "path": "/api/error-codes-accounts",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-accounts",
    "pageTitle": "Error codes: accounts and sessions",
    "section": "Related",
    "anchor": "related",
    "text": "Authentication: keys and sessions The error format"
  },
  {
    "id": "api/error-codes-organization",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Error codes: organization, access, joining",
    "anchor": "",
    "text": "The codes of the Tenancy, Access, Onboarding, and Cloud-Connections areas: master data and brand of the organization, custom domain, company sign-in, roles and grants, invitations, sign-up, join requests, and storage connections."
  },
  {
    "id": "api/error-codes-organization#how-to-read-the-tables",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Read the tables like this: Code is the value of the code field in the error body. Your logic switches on it, never on message . Meaning and Typical trigger classify the case. HTTP is the status actually sent. Expected reaction is your client’s response, drawn from a fixed vocabulary that is the same on all seven error-code pages: “check credentials” (401), “check permission or package” (402, 403), “correct the input” (400, 413, 415, 422), “reload, resend” (version conflicts), “do not retry, check the operation” (404, 409, 410, 426), “wait (Retry-After)” (429), and “retry later, report the trace ID if it recurs” (5xx)."
  },
  {
    "id": "api/error-codes-organization#the-codes",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-organization#organization-tenancy",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Organization (Tenancy)",
    "anchor": "organization-tenancy",
    "text": "Master data, brand, address, company sign-in including its domains, and the portal’s own address. Tenant.Frozen (403) blocks changes while a payment is outstanding. Reading and billing stay open. Code Meaning Typical trigger HTTP Expected reaction Tenant.NotFound Organization not found Unknown ID of the organization 404 do not retry, check the operation Tenant.SlugAlreadyExists Short name already taken Creation with a short name that is already in use 409 do not retry, check the operation Tenant.SlugReserved Short name reserved Reserved short name 422 correct the input Tenant.InvalidStatusTransition Invalid status change of the organization Disallowed status change 409 do not retry, check the operation Tenant.Frozen Organization frozen Change while a payment is outstanding. Reading and billing stay open 403 check permission or package Tenant.DeleteConfirmationRequired Deletion requires confirmation Deletion without confirmation 422 correct the input Branding.ContrastInsufficient Contrast insufficient Color choice falls below the minimum contrast 422 correct the input Branding.AssetNotFound Brand mark missing Reference to an unknown logo/favicon 422 correct the input Address.InvalidCountryCode Country code invalid Unknown ISO country code 422 correct the input Sso.ConfigInvalid Company sign-in configuration invalid Malformed company sign-in fields, or the issuer does not respond with a valid provider description 422 correct the input Sso.DefaultRoleInvalid Default role invalid"
  },
  {
    "id": "api/error-codes-organization#roles-and-grants-access",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Roles and grants (Access)",
    "anchor": "roles-and-grants-access",
    "text": "Roles, grants, and permission keys. Built-in roles are immutable. A role can never carry permissions that the acting person does not have. Code Meaning Typical trigger HTTP Expected reaction Role.NotFound Role not found Unknown role ID 404 do not retry, check the operation Role.BuiltInImmutable System role immutable Change to a built-in role 409 do not retry, check the operation Role.InUse Role in use Deleting a role that someone still holds or that a sign-in method uses as its default 409 do not retry, check the operation Role.PermissionNotInCatalog Permission not in the catalog Role with an unknown permission key 422 correct the input Role.PermissionEscalationBlocked Permissions beyond your own set Role with permissions that the acting person does not hold 403 check permission or package Grant.NotFound Grant not found Unknown grant ID 404 do not retry, check the operation Grant.AllowDenyConflict Allow-deny conflict Contradictory grant rules 422 correct the input Grant.ResourceNotOwned Resource not owned Grant on a resource that belongs to someone else 403 check permission or package PermissionKey.InvalidSyntax Permission key malformed Malformed permission key 422 correct the input"
  },
  {
    "id": "api/error-codes-organization#invitations-and-sign-up-onboarding",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Invitations and sign-up (Onboarding)",
    "anchor": "invitations-and-sign-up-onboarding",
    "text": "Invitations, setup wizard, creation of an organization, self sign-up, and join requests. Two deviations from the area rule are intentional: Signup.TicketInvalid answers 401 because the ticket is the proof, and Signup.Disabled answers 403, since the switch-off takes effect only where a ticket exists. Sign-up deliberately reveals nothing about what exists: an unknown, a used, and an expired confirmation code all answer identically with Signup.TicketInvalid . Code Meaning Typical trigger HTTP Expected reaction Invitation.NotFound Invitation not found Unknown invitation ID 404 do not retry, check the operation Invitation.TokenInvalid Invitation token invalid Malformed token 400 correct the input Invitation.Expired Invitation expired Expired token 410 do not retry, check the operation Invitation.AlreadyAccepted Invitation already accepted Duplicate acceptance 409 do not retry, check the operation Invitation.Revoked Invitation revoked Redeemed but revoked invitation 410 do not retry, check the operation Invitation.RoleNotInTenant Role not in the organization Invitation with a role of another organization 422 correct the input SetupStep.Unknown Setup step unknown Invalid step of the setup wizard 422 correct the input Provisioning.SagaFailed Creation of the organization failed Error in the multi-step creation of an organization 500 retry later, report the trace ID if it recurs Provisioning.OwnerEmailAlreadyExists Owner’s address taken Creating an organization with an address that is "
  },
  {
    "id": "api/error-codes-organization#storage-connections-cloud-connections",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Storage connections (Cloud-Connections)",
    "anchor": "storage-connections-cloud-connections",
    "text": "The authorizations on third-party storage accounts, from which the media library draws its places. The 409 codes share one message: the connection exists, but its state does not hold up, and the answer is a new round trip, not troubleshooting. Code Meaning Typical trigger HTTP Expected reaction CloudConnection.NotFound Connection not found Disconnecting, repairing, or fetching a token with an unknown or foreign connection ID — a foreign ID and an invented one are deliberately indistinguishable 404 do not retry, check the operation CloudConnection.ProviderUnavailable Provider not set up Connecting to a storage provider for which this deployment has no configuration stored 404 do not retry, check the operation CloudConnection.StateInvalid Callback state unknown The callback carries no state that this server started: unknown, expired, or already redeemed — all three answer the same 401 check credentials CloudConnection.CallbackFailed Callback without authorization Code exchange failed, the provider grants no offline access, or the account lookup failed 401 check credentials CloudConnection.AccountMismatch A different account came back A repair round trip that signs in with a different account than the connection’s. An error of its own instead of silently creating a new entry — otherwise two entries would exist, one of which would never work again 409 do not retry, check the operation CloudConnection.Broken Authorization no longer holds The provider rejects the authorization. Onl"
  },
  {
    "id": "api/error-codes-organization#count",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 51 codes . Together with the other six error-code pages this gives 493 domain codes (4xx) and 119 server errors (5xx), 612 codes in all — the complete catalog of the platform."
  },
  {
    "id": "api/error-codes-organization#related",
    "path": "/api/error-codes-organization",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-organization",
    "pageTitle": "Error codes: organization, access, joining",
    "section": "Related",
    "anchor": "related",
    "text": "The error format Handbook: Organization"
  },
  {
    "id": "api/error-codes-billing",
    "path": "/api/error-codes-billing",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-billing",
    "pageTitle": "Error codes: billing",
    "section": "Error codes: billing",
    "anchor": "",
    "text": "The codes of the Billing area: package, licenses, add-on bookings, payment methods, invoices, orders, and the responses of the payment service provider."
  },
  {
    "id": "api/error-codes-billing#how-to-read-the-tables",
    "path": "/api/error-codes-billing",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-billing",
    "pageTitle": "Error codes: billing",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Read the tables like this: Code is the value of the code field in the error body. Your logic switches on it, never on message . Meaning and Typical trigger classify the case. HTTP is the status actually sent. Expected reaction is your client’s response, drawn from a fixed vocabulary that is the same on all seven error-code pages: “check credentials” (401), “check permission or package” (402, 403), “correct the input” (400, 413, 415, 422), “reload, resend” (version conflicts), “do not retry, check the operation” (404, 409, 410, 426), “wait (Retry-After)” (429), and “retry later, report the trace ID if it recurs” (5xx)."
  },
  {
    "id": "api/error-codes-billing#the-codes",
    "path": "/api/error-codes-billing",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-billing",
    "pageTitle": "Error codes: billing",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-billing#billing",
    "path": "/api/error-codes-billing",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-billing",
    "pageTitle": "Error codes: billing",
    "section": "Billing",
    "anchor": "billing",
    "text": "The status codes carry meaning here: 402 marks blocks that a booking resolves, namely payment method, contract limit, licenses, add-on booking, app, and package. 403 Billing.FeatureNotEntitled means that a contractual capability is missing, such as API access or webhooks. Only a different package or the operator resolves this, not a booking in the portal. Which package contains which capability is described in the handbook under Billing . 409 marks cases where something else has to be done first (billing profile, payment method, sequence of steps). The Stripe.* codes keep their literal names because that is how they appear in the catalog. Code Meaning Typical trigger HTTP Expected reaction Subscription.NotFound Contract not found Unknown contract ID 404 do not retry, check the operation Subscription.AlreadyExists Contract already exists Duplicate creation 409 do not retry, check the operation Subscription.InvalidStatusTransition Invalid contract transition Disallowed status change 409 do not retry, check the operation PaymentMethod.Required Payment method required Action without a payment method on file 402 check permission or package Quota.Exceeded Limit exceeded Contractual limit reached 402 check permission or package Billing.LicensesExhausted All licenses in use Pairing another display although the contract no longer carries a free license. The detail names booked and in use, so that the interface can show the way to book more 402 check permission or package Billing.Licen"
  },
  {
    "id": "api/error-codes-billing#count",
    "path": "/api/error-codes-billing",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-billing",
    "pageTitle": "Error codes: billing",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 51 codes . Together with the other six error-code pages this gives 493 domain codes (4xx) and 119 server errors (5xx), 612 codes in all — the complete catalog of the platform."
  },
  {
    "id": "api/error-codes-billing#related",
    "path": "/api/error-codes-billing",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-billing",
    "pageTitle": "Error codes: billing",
    "section": "Related",
    "anchor": "related",
    "text": "The error format Create a webhook endpoint Handbook: Billing"
  },
  {
    "id": "api/error-codes-content",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Error codes: media, scenes, programs, apps",
    "anchor": "",
    "text": "The codes of the Media-Library, Composition, Scheduling, App-Catalog, Marketplace, App-Runtime, Feeds, and Advertising areas: media and folders, scenes and templates, playlists and programs, app releases, installations, the runtime of installed apps, the catalog of news sources, and advertising."
  },
  {
    "id": "api/error-codes-content#how-to-read-the-tables",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Read the tables like this: Code is the value of the code field in the error body. Your logic switches on it, never on message . Meaning and Typical trigger classify the case. HTTP is the status actually sent. Expected reaction is your client’s response, drawn from a fixed vocabulary that is the same on all seven error-code pages: “check credentials” (401), “check permission or package” (402, 403), “correct the input” (400, 413, 415, 422), “reload, resend” (version conflicts), “do not retry, check the operation” (404, 409, 410, 426), “wait (Retry-After)” (429), and “retry later, report the trace ID if it recurs” (5xx)."
  },
  {
    "id": "api/error-codes-content#the-codes",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-content#media-and-folders-media-library",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Media and folders (Media-Library)",
    "anchor": "media-and-folders-media-library",
    "text": "Media, folders, uploads, dynamic collections, as well as the places of third-party storage and their folder syncs. The three upload codes Upload.MimeTypeNotAllowed , Upload.QuotaExceeded , and Upload.Sha256Mismatch are the ones a script runs into when uploading. The CLI ends a rejected upload with exit 11 — see JSON output and exit codes . Code Meaning Typical trigger HTTP Expected reaction Asset.NotFound Media item not found Unknown media ID 404 do not retry, check the operation Asset.InUse Media item in use Deleting a media item that is in use 409 do not retry, check the operation Asset.NotInTrash Media item is not in the trash Permanently deleting a media item that is still active 409 do not retry, check the operation Asset.InvalidStatusTransition Status change not permitted Reprocessing a media item that is currently being uploaded or processed 409 do not retry, check the operation AssetVariant.NotFound Variant does not exist URL request for a variant that was never created 404 do not retry, check the operation Folder.NotFound Folder not found Unknown folder ID 404 do not retry, check the operation Folder.MaxDepthExceeded Maximum folder depth exceeded Nesting too deep 422 correct the input Folder.CycleDetected Folder cycle detected Moving into its own subtree 422 correct the input Folder.Protected Brand folder is write-protected Renaming, moving, or deleting in the system folder “Brand marks”. Reading, searching, and using in scenes remain unrestricted — only writing is c"
  },
  {
    "id": "api/error-codes-content#scenes-and-templates-composition",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Scenes and templates (Composition)",
    "anchor": "scenes-and-templates-composition",
    "text": "Renderer sessions, scenes, layout templates, widgets, and integrity. Version conflicts when saving go through Conflict.OptimisticLock , not through a code of this area. Code Meaning Typical trigger HTTP Expected reaction RendererSession.NotFound Renderer session not found Unknown session ID 404 do not retry, check the operation RendererSession.AlreadyRevoked Renderer session already revoked Duplicate revocation 409 do not retry, check the operation Scene.NotFound Scene not found Unknown scene ID 404 do not retry, check the operation Scene.PublishValidationFailed Publishing invalid Scene does not meet the rules for publishing 422 correct the input Scene.PublishBlocked Publishing blocked Open integrity problems 409 do not retry, check the operation Scene.Blocked Scene blocked by block cascade New assignment (program, override, playlist) of a scene that uses an app that the booked package no longer covers. Listed here and not under Scheduling because the SCENE is blocked. 422 instead of 403 because two ways lead out: upgrade the package or take the app out of the scene 422 correct the input Scene.InUse Scene in use Deleting a scene that is in use 409 do not retry, check the operation SceneVersion.NotFound Scene version not found Unknown version 404 do not retry, check the operation SceneVersion.ThumbnailNotFound No thumbnail for the version Fetching the thumbnail of a version for which none is stored — a regular case, not a defect: versions from before the automatic capture have"
  },
  {
    "id": "api/error-codes-content#playlists-and-programs-scheduling",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Playlists and programs (Scheduling)",
    "anchor": "playlists-and-programs-scheduling",
    "text": "Playlists, programs, and the assignment on the display. Schedule.ContentNotFound covers scene, playlist, and media item in one code. Code Meaning Typical trigger HTTP Expected reaction Playlist.NotFound Playlist not found Unknown playlist ID 404 do not retry, check the operation Playlist.Empty Playlist empty Use without entries 422 correct the input Playlist.InUse Playlist in use Deleting a playlist that is in use 409 do not retry, check the operation Playlist.SceneNotFound Scene of the playlist missing Entry points to an unknown scene 422 correct the input Playlist.AssetNotFound Media item of the playlist missing Entry points to an unknown media item 422 correct the input Playlist.AutoSourceItemsForbidden Automatic playlist with entries Folder/tag source with a maintained sequence 422 correct the input Playlist.SourceNotAuto Source not automatic Source preview for a manual playlist 422 correct the input Schedule.NotFound Program not found Unknown program ID 404 do not retry, check the operation Schedule.ContentNotFound Program content missing Reference to an unknown scene, playlist, or media item 422 correct the input Schedule.InvalidRecurrence Invalid repeat Malformed repeat rule of a program entry 422 correct the input Schedule.Assigned Program is assigned Deleting a program that is still running on a device 409 do not retry, check the operation Schedule.ItemNotFound Program entry not found Unknown entry ID or entry of a different program 404 do not retry, check the operat"
  },
  {
    "id": "api/error-codes-content#app-catalog-app-catalog",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "App catalog (App-Catalog)",
    "anchor": "app-catalog-app-catalog",
    "text": "Releases, manifest, the import from the registry, and channels, the operator side of the catalog. A version arrives in the catalog as a signed artifact from the registry. If the artifact is not in order, the import answers with AppRelease.RegistryImportFailed (422), and without access to the registry with AppRelease.RegistryUnavailable (503). No single person promotes to stable: one person requests, a second confirms, and the AppReleaseApproval.* codes belong to this path. How a version is signed and which calls take it through the channels is described under Signing and the path into the catalog . Code Meaning Typical trigger HTTP Expected reaction AppRelease.NotFound Release not found Unknown release ID 404 do not retry, check the operation AppRelease.VersionAlreadyExists Version already exists The version is already in the catalog with different bytes 409 do not retry, check the operation AppRelease.AlreadyRetired Release already retired Duplicate retirement 409 do not retry, check the operation Manifest.ValidationFailed Manifest invalid Manifest violates the schema 422 correct the input Channel.InvalidTransition Invalid channel transition Disallowed channel change (no skipping) 409 do not retry, check the operation AppStatusOverride.NotFound Special rule not found Withdrawing a rule that does not exist for this pair of organization and app 404 do not retry, check the operation AppRelease.RegistryArtifactNotFound Version not in the registry Importing a version whose tag do"
  },
  {
    "id": "api/error-codes-content#marketplace",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Marketplace",
    "anchor": "marketplace",
    "text": "Installation and grants from the organization’s point of view, plus requests to the Owners and the deletion of the data of an uninstalled app. Code Meaning Typical trigger HTTP Expected reaction App.NotFound App not found Unknown app ID 404 do not retry, check the operation App.NotVisible App not visible No access to the app 403 check permission or package Grant.AlreadyInstalled Already installed Duplicate installation 409 do not retry, check the operation App.NotInstalled App not installed Action without an installation 409 do not retry, check the operation Grant.PendingReacceptance Renewed consent pending Use before consent is given again 409 do not retry, check the operation App.ManifestHashMismatch Manifest hash differs Consent to a different manifest 409 do not retry, check the operation App.UpdateUnavailable No such update Switching to a version that is not above the installed one, accepting or declining without a pending update, declining after the deadline has passed. details.reason names the case 409 do not retry, check the operation OwnerRequest.NotApplicable Nothing to decide Request to the Owners for an app that is already installed, for an update that is not pending, or from an account that may decide on its own. details.reason names installed , no-pending-update , or can-decide 409 do not retry, check the operation AppData.NotDeletable Data cannot be deleted Scheduling the deletion of the data of an installed app, of an app without data, or of a deletion that is"
  },
  {
    "id": "api/error-codes-content#app-runtime-app-runtime",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "App runtime (App-Runtime)",
    "anchor": "app-runtime-app-runtime",
    "text": "Fetch cache and scheduled jobs of installed apps. Fetch.Failed is a deliberate 502 among the domain codes: an external source did not respond in a usable way. Code Meaning Typical trigger HTTP Expected reaction FetcherCache.NotFound Cache entry not found Unknown entry in the fetch cache 404 do not retry, check the operation FetchKey.Invalid Fetch key invalid Unknown or malformed fetch key 422 correct the input Fetch.Failed Data fetch failed Error when fetching from an external source 502 retry later, report the trace ID if it recurs JobSchedule.NotFound Job schedule not found Unknown schedule ID 404 do not retry, check the operation Job.CronInvalid Schedule expression invalid Malformed schedule expression 422 correct the input"
  },
  {
    "id": "api/error-codes-content#news-sources-feeds",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "News sources (Feeds)",
    "anchor": "news-sources-feeds",
    "text": "The catalog of news sources, which the operator maintains and the news apps read. Code Meaning Typical trigger HTTP Expected reaction FeedSource.NotFound News source not found Changing a source that is not in the catalog. details.sourceId names it 404 do not retry, check the operation"
  },
  {
    "id": "api/error-codes-content#advertising",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Advertising",
    "anchor": "advertising",
    "text": "Advertisers, ad campaigns, bookings, creatives, ad space, audience, participation in the ad network, and the revenue statements of the locations. A foreign ID and an invented ID answer alike with 404. This way, your organization never learns whether an ad campaign of another organization or of the ad network exists. Without the capability advertisingDirect , creating and changing in your own advertising business answer with Billing.FeatureNotEntitled (403). Reading, pausing, ending, and archiving stay open so that running ad campaigns can wind down. Code Meaning Typical trigger HTTP Expected reaction Advertiser.NotFound Advertiser not found Unknown ID or an advertiser of another organization 404 do not retry, check the operation Advertiser.Archived Advertiser archived A new ad campaign, booking, or creative, booking or resuming an ad campaign, or changing a booking (except pausing) for an archived advertiser 409 do not retry, check the operation AdCampaign.NotFound Ad campaign not found Unknown ID, an ad campaign of another organization, or one of the ad network that does not reach your displays 404 do not retry, check the operation AdCampaign.InvalidPeriod Period reversed The ad campaign starts after its end 422 correct the input AdCampaign.StatusTransitionInvalid Transition not allowed Going back to draft, or resuming an ended ad campaign 409 do not retry, check the operation AdCampaign.Cancelled Ad campaign ended Changing an ended ad campaign or its bookings 409 do not ret"
  },
  {
    "id": "api/error-codes-content#count",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 120 codes . Together with the other six error-code pages this gives 493 domain codes (4xx) and 119 server errors (5xx), 612 codes in all — the complete catalog of the platform."
  },
  {
    "id": "api/error-codes-content#related",
    "path": "/api/error-codes-content",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-content",
    "pageTitle": "Error codes: media, scenes, programs, apps",
    "section": "Related",
    "anchor": "related",
    "text": "The error format JSON output and exit codes Signing and the path into the catalog"
  },
  {
    "id": "api/error-codes-devices-operations",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Error codes: devices, playback, notifications, operations",
    "anchor": "",
    "text": "The codes of the Fleet, Playback, Realtime, Notifications, Analytics, and Audit areas, and those of the operations console, which arise only on platform-operations routes."
  },
  {
    "id": "api/error-codes-devices-operations#how-to-read-the-tables",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Read the tables like this: Code is the value of the code field in the error body. Your logic switches on it, never on message . Meaning and Typical trigger classify the case. HTTP is the status actually sent. Expected reaction is your client’s response, drawn from a fixed vocabulary that is the same on all seven error-code pages: “check credentials” (401), “check permission or package” (402, 403), “correct the input” (400, 413, 415, 422), “reload, resend” (version conflicts), “do not retry, check the operation” (404, 409, 410, 426), “wait (Retry-After)” (429), and “retry later, report the trace ID if it recurs” (5xx)."
  },
  {
    "id": "api/error-codes-devices-operations#the-codes",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-devices-operations#devices-and-displays-fleet",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Devices and displays (Fleet)",
    "anchor": "devices-and-displays-fleet",
    "text": "Displays, pairing, commands, tags, video walls, locations, and updates. Device.NotPaired (409) is a state that passes on its own: the device is registered but not yet assigned to an organization. Code Meaning Typical trigger HTTP Expected reaction Device.NotPaired Device not yet assigned to an organization Self-report of a freshly registered agent (capabilities, events, screenshots) before anyone has redeemed the pairing code in the portal. A code of its own, because as Player.NotFound this preliminary phase could not be told apart from an outage — it passes on its own 409 do not retry, check the operation Player.NotFound Display not found Unknown display ID 404 do not retry, check the operation Player.AlreadyDecommissioned Display already removed Action on a removed display, including a second removal 409 do not retry, check the operation Player.ConfigInvalid Display configuration invalid Malformed configuration 422 correct the input Player.OutputNotAvailable No reported connector at this position “Add display” on an output that the device has not reported as occupied (cable missing, or the capability report is still pending) 409 do not retry, check the operation Player.ScreenIndexTaken Position already occupied “Add display” on a connector that already has a display attached 409 do not retry, check the operation Player.ScreenshotNotFound No screenshot stored Fetching the screenshot as bytes for a display that has not sent one yet or whose screenshot has been cleaned up 404 "
  },
  {
    "id": "api/error-codes-devices-operations#playback",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Playback",
    "anchor": "playback",
    "text": "Playback packages and the reports of the display software. Code Meaning Typical trigger HTTP Expected reaction Snapshot.NotFound Playback package not found Unknown playback package of a display 404 do not retry, check the operation Renderer.DiagnosticsRateLimited Diagnostics rate exceeded Diagnostic reports sent too often 429 wait (Retry-After)"
  },
  {
    "id": "api/error-codes-devices-operations#real-time-connections-realtime",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Real-time connections (Realtime)",
    "anchor": "real-time-connections-realtime",
    "text": "Only Connection.NotFound occurs regularly as an HTTP error. The other four codes are reasons for rejecting the real-time connection, without an error body. The HTTP column gives their equivalent in case they do reach an HTTP path. The real-time connection cannot be reached with an API key. Code Meaning Typical trigger HTTP Expected reaction Connection.AuthFailed Sign-in of the connection failed Opening the real-time connection without a valid credential 401 check credentials Connection.NotFound Connection not found Unknown connection ID 404 do not retry, check the operation Connection.RateLimited Connection rate exceeded Too many connection attempts 429 wait (Retry-After) Subscription.PermissionDenied Permission for the topic missing No access to the topic 403 check permission or package Topic.InvalidFormat Topic format invalid Malformed topic name 422 correct the input"
  },
  {
    "id": "api/error-codes-devices-operations#notifications-and-webhooks-notifications",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Notifications and webhooks (Notifications)",
    "anchor": "notifications-and-webhooks-notifications",
    "text": "Inbox, delivery channels, email, and webhooks. The five webhook codes concern the management of endpoints: WebhookEndpoint.NotFound (404), WebhookEndpoint.Disabled (409), WebhookEndpoint.UrlInvalid (422), WebhookEndpoint.EventUnknown (422), and WebhookDelivery.NotFound (404). What to do when deliveries fail to arrive is described under When no delivery arrives . Email.ProviderUnavailable is a deliberate 502 among the domain codes. Code Meaning Typical trigger HTTP Expected reaction Notification.NotFound Notification not found Unknown notification ID 404 do not retry, check the operation Preferences.InvalidChannel Invalid channel Unknown notification channel 422 correct the input WebhookEndpoint.NotFound Webhook endpoint not found Unknown endpoint ID 404 do not retry, check the operation WebhookEndpoint.Disabled Webhook endpoint disabled Use of a disabled endpoint 409 do not retry, check the operation WebhookEndpoint.UrlInvalid Webhook URL invalid Malformed target URL 422 correct the input WebhookEndpoint.EventUnknown Event type unknown An entry in events is not in the event catalog and does not have the form <area>.* or * either. The response lists the unknown names under details.unknownEvents 422 correct the input WebhookDelivery.NotFound Delivery not found Unknown delivery ID 404 do not retry, check the operation Email.ProviderUnavailable Email provider unreachable Outage of the mail delivery 502 retry later, report the trace ID if it recurs Email.TemplateNotFound Email tem"
  },
  {
    "id": "api/error-codes-devices-operations#analytics",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Analytics",
    "anchor": "analytics",
    "text": "Reports and queries. Dimension.Unknown replaces a silent empty result: the detail names the permitted axes. The ad hoc analysis checks every query against the metric catalog: AnalyticsQuery.NotAllowed names in details.reason the same reason that GET /v1/analytics/catalog returns for each combination of metric and dimension. An expired or revoked link answers with 410 and not with 404: The link existed, it just no longer applies. Code Meaning Typical trigger HTTP Expected reaction AnalyticsReport.NotFound Report not found Unknown report ID 404 do not retry, check the operation GeneratedReport.NotFound Generated report not found Unknown report run 404 do not retry, check the operation GeneratedReport.Expired Generated report expired Expired download 410 do not retry, check the operation AnalyticsQuery.Invalid Query invalid Malformed analytics query 422 correct the input Metric.Unknown Metric unknown The metric does not exist 422 correct the input Dimension.Unknown Axis does not carry the metric Breakdown by a dimension that the metric catalog does not list for this metric. The code replaces silent emptiness : before, an empty result came back without any hint. The detail names the permitted axes 422 correct the input AnalyticsQuery.CardinalityExceeded Too many value combinations Query with too many axes 422 correct the input AnalyticsQuery.NotAllowed Combination not allowed A metric and dimension or filter that the metric catalog does not calculate together, for example fulfill"
  },
  {
    "id": "api/error-codes-devices-operations#audit-log-audit",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Audit log (Audit)",
    "anchor": "audit-log-audit",
    "text": "Log entries, exports, and pseudonymization from the organization’s point of view. Code Meaning Typical trigger HTTP Expected reaction AuditEntry.NotFound Log entry not found Unknown entry ID 404 do not retry, check the operation AuditExport.NotFound Log export not found Unknown export ID 404 do not retry, check the operation AuditExport.NotReady Log export not ready Download before completion 409 do not retry, check the operation Audit.CrossTenantForbidden Cross-organization access forbidden Access to the audit log of another organization 403 check permission or package Pseudonymization.ReasonRequired Reason required Pseudonymization without a reason (≥20 characters) 422 correct the input Pseudonymization.JobNotFound Pseudonymization job missing Unknown job ID 404 do not retry, check the operation Pseudonym.NotFound Pseudonym not found Lookup without a match 404 do not retry, check the operation"
  },
  {
    "id": "api/error-codes-devices-operations#operations-console-ops-console",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Operations console (Ops-Console)",
    "anchor": "operations-console-ops-console",
    "text": "These codes arise exclusively on platform-operations routes: the device software including the channel of a device, maintenance windows, and incidents. A key of an organization cannot reach these routes. The table is here for completeness. Code Meaning Typical trigger HTTP Expected reaction DeviceSoftware.ComponentNotSupported Component without a delivery path Fetching, releasing, or delivering a component that this version of the platform does not deliver yet. Today that is everything except the display software 422 correct the input DeviceSoftware.BuildNotFound Build not in storage Releasing a build that the environment has not yet fetched from the registry 404 do not retry, check the operation DeviceSoftware.ArtifactNotFound Archive not in storage Fetching an archive with an unknown component, build, or name 404 do not retry, check the operation DeviceSoftware.RegistryArtifactNotFound Build not in the registry The tag does not (or no longer) exist in the registry 404 do not retry, check the operation DeviceSoftware.RegistryImportFailed Fetching from the registry failed Access missing, the manifest is incomplete, a checksum differs, a profile is missing, or the description in the archive contradicts the manifest. The detail names the cause 422 correct the input DeviceSoftware.NotReleasable Build cannot be released The build requires a version of the interface that the environment does not speak, or a profile or archive is missing 422 correct the input DeviceSoftware.Signing"
  },
  {
    "id": "api/error-codes-devices-operations#count",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 102 codes . Together with the other six error-code pages this gives 493 domain codes (4xx) and 119 server errors (5xx), 612 codes in all — the complete catalog of the platform."
  },
  {
    "id": "api/error-codes-devices-operations#related",
    "path": "/api/error-codes-devices-operations",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-devices-operations",
    "pageTitle": "Error codes: devices, playback, notifications, operations",
    "section": "Related",
    "anchor": "related",
    "text": "The error format When no delivery arrives"
  },
  {
    "id": "api/error-codes-server",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Error codes: server errors (5xx)",
    "anchor": "",
    "text": "The codes a client cannot react to specifically: violated invariants, startup errors, the database, and downstream services. There is no reaction column here, because the reaction is the same within each section."
  },
  {
    "id": "api/error-codes-server#how-to-read-the-tables",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Code is the value of the code field in the error body, HTTP the status sent. There is no column for the reaction here because it is the same within each section: for 500 , you do not retry and you report the traceId from the error body. For 502 , 503 , and 504 , you retry later and honor retryAfterSeconds in the body and Retry-After in the headers. If the same error comes back, report the trace ID. Nine codes send a 5xx and are still not listed here but under their domain area, because they are set there deliberately: a specific downstream service fails or the house is not ready to compute for a moment, and the caller knows the reason. They are Provisioning.SagaFailed (500) under Organization, access, joining , Stripe.AdapterError (502), Billing.VatIdCheckUnavailable (503), Billing.PriceBackfillPending (503), and Billing.EInvoiceGuardFailed (500) under Billing , Social.ProviderUnavailable (503) under Accounts and sessions , Email.ProviderUnavailable (502) under Devices, playback, notifications, operations , and AppRelease.RegistryUnavailable (503) and Fetch.Failed (502) under Media, scenes, programs, apps ."
  },
  {
    "id": "api/error-codes-server#the-codes",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "The codes",
    "anchor": "the-codes",
    "text": ""
  },
  {
    "id": "api/error-codes-server#general-internal-errors",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "General (Internal, Errors)",
    "anchor": "general-internal-errors",
    "text": "Unclassified exceptions and defects in the error catalog itself. Reaction: do not retry, report the trace ID. Code Meaning Typical trigger HTTP Internal.Unexpected Unexpected error Unclassified exception 500 Errors.CatalogInconsistent Error catalog inconsistent Catalog validation failed 500 Errors.UnknownCode Unknown error code Unregistered code 500 Errors.SerializationFailed Error serialization failed Response cannot be serialized 500"
  },
  {
    "id": "api/error-codes-server#permissions-at-runtime-and-at-startup",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Permissions at runtime and at startup",
    "anchor": "permissions-at-runtime-and-at-startup",
    "text": "Misconfigurations of the permission check, which show up at startup or on a single route. Reaction: do not retry, report the trace ID. Code Meaning Typical trigger HTTP Internal.AuthGuardConfigError Authentication check misconfigured Faulty wiring of the authentication check 500 Internal.PermissionPipelineMisconfigured Permission check chain misconfigured Missing components of the check chain 500 Internal.PermissionResourceParamMissing Resource parameter missing Route description incomplete 500 Internal.PermissionResourceIdMalformed Resource ID malformed Invalid ID during resolution 500 Internal.PermissionResourceTypeUnknown Resource type unknown Unregistered type 500 Internal.PermissionCatalogInconsistent Permission catalog inconsistent Catalog validation failed 500 Internal.PermissionMetadataMalformed Permission metadata malformed Invalid metadata 500 Internal.PermissionRuntimeUnknownActorKind Unknown kind of caller Caller kind not supported 500"
  },
  {
    "id": "api/error-codes-server#database",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Database",
    "anchor": "database",
    "text": "Violated rules of the database layer, such as a query without the context of an organization. The codes carry the name of the database library literally, because that is how they appear in the catalog. Reaction: do not retry, report the trace ID. Code Meaning Typical trigger HTTP Prisma.TenantScopeMissing Organization context missing Query without the context of an organization 500 Prisma.RawQueryOnTenantIsolatedModel Raw query on a separately managed model Bypassing the separation between organizations 500 Prisma.AsPlatformNoRequestScope Platform query without request context Platform query without context 500 Prisma.AsPlatformInvalidReason Invalid reason for a platform query Missing or empty reason 500 Prisma.NestedTransactionError Nested transaction Transaction inside a transaction 500 Prisma.UnknownApp Unknown app (database) App client for an unknown app 500 Prisma.AppHasNoSchema App without a schema App query without a schema of its own 500 Prisma.AppClientLoadFailed Loading the app client failed Error while loading the app client 500 Prisma.SchemaRegistryFileNotFound Schema registry file missing Startup without the registry file 500 Prisma.SchemaRegistryInvalidFormat Schema registry invalid Malformed registry format 500 Prisma.SchemaRegistryDuplicate Schema registry duplicate Duplicate registry entry 500 Prisma.BootSchemaValidationFailed Schema check at startup failed Schema check at startup 500 Prisma.UnknownOperation Unknown database operation Unsupported operation 50"
  },
  {
    "id": "api/error-codes-server#other-areas",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Other areas",
    "anchor": "other-areas",
    "text": "HTTP, input validation, request context, events, cache, observability, OpenAPI, audit log, jobs, apps, and contracts. All answer 500 except two: Internal.HttpRequestTimeout (504) and Internal.OpenApiNotInitialized (503). For these two a later attempt makes sense, for all others it does not. Code Meaning Typical trigger HTTP Internal.HttpResponseSerializationFailed HTTP response cannot be serialized Error while building the response 500 Internal.HttpRequestTimeout Request timed out Request timeout 504 Internal.HttpModuleOptionsInvalid HTTP module options invalid Misconfiguration at startup 500 Internal.HttpPrismaFilterMisconfigured Database error filter misconfigured Misconfiguration at startup 500 Internal.ValidationModuleOptionsInvalid Options of the validation module invalid Misconfiguration at startup 500 Internal.ValidationAllowlistConsistencyFailed Allowlist inconsistent Check at startup failed 500 Internal.ValidationProtocolConsistencyFailed Protocol consistency violated Check at startup failed 500 Internal.RequestScopeNotInitialized Request context not initialized Access outside the context 500 Internal.RequestScopeIdentityMissing Identity in the request context missing Context without an identity 500 Internal.RequestScopeOptionsInvalid Options of the request context invalid Misconfiguration at startup 500 Events.MissingRequestScope Event without request context Triggering outside the context 500 Events.AlreadyAppended Event already appended Duplicate storage in the ev"
  },
  {
    "id": "api/error-codes-server#downstream-services-integration",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Downstream services (Integration)",
    "anchor": "downstream-services-integration",
    "text": "A third-party service or a component of the platform’s own infrastructure does not respond. Errors passed through from a third-party service answer 502 , the outage of a component owned by the platform 503 . Reaction for both: retry later and honor retryAfterSeconds and Retry-After . If it persists, report the trace ID. Code Meaning Typical trigger HTTP Integration.VaultUnreachable Secrets store unreachable Outage of the secrets store 502 Integration.JwtKeyStoreUnavailable Store of the signing keys unavailable Signing keys cannot be loaded 503 Integration.MtlsCaUnavailable Certificate authority of the devices unavailable Certificate authority unreachable 503 Integration.IdentityResolverFailed Identity resolution failed Error in identity resolution 503 Integration.AccessReadModelUnreachable Read view of the permissions unreachable Outage of the read view of the permissions 503 Integration.PrismaConnectionLost Database connection lost Connection to the database dropped 502 Integration.PrismaQueryTimeout Database query timed out Timeout of a database query 502 Integration.PrismaTransactionTimeout Database transaction timed out Transaction timeout 502 Integration.PrismaTransactionMaxWait Wait time of a database transaction Wait-time limit exceeded 502 Integration.PrismaForeignKey Foreign key error in the database Reference to a record that does not exist, at database level 502 Integration.OutboxDispatcherTimeout Timeout of the event dispatcher Timeout during delivery 502 Integrat"
  },
  {
    "id": "api/error-codes-server#count",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Count",
    "anchor": "count",
    "text": "This page lists 119 codes — all server errors in the catalog. Together with the 493 domain codes on the six other error-code pages, there are 612 codes in all."
  },
  {
    "id": "api/error-codes-server#related",
    "path": "/api/error-codes-server",
    "area": "api",
    "areaTitle": "API",
    "page": "error-codes-server",
    "pageTitle": "Error codes: server errors (5xx)",
    "section": "Related",
    "anchor": "related",
    "text": "The error format Release trains, notes, and deprecations"
  },
  {
    "id": "api/read-reference",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "Read the reference",
    "anchor": "",
    "text": "This guide shows you how to find an operation in the reference at /api/reference , read off the required permission, the security scheme, the example, and the stability, and build a curl call from them."
  },
  {
    "id": "api/read-reference#what-the-reference-shows",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "What the reference shows",
    "anchor": "what-the-reference-shows",
    "text": "The reference is the public specification of the current release, displayed interactively: 443 operations in 19 areas and 7 groups, each operation with a short description in German, each schema with an example from a coherent demo universe. Whatever the specification lists as internal appears neither in the reference nor in the file /api/openapi.json , including the entire operator surface under /v1/platform/… , the bridges to the device and the display software, and the operational routes under /health/… ."
  },
  {
    "id": "api/read-reference#steps",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "Steps",
    "anchor": "steps",
    "text": "In the left navigation, choose the group and, below it, the area. The seven groups are Identity & access , Organization & billing , Content & planning , Apps & marketplace , Fleet & playback , Monitoring & notifications , and Platform operations . The first six are the surface for organizations. Under Platform operations , only the two operational routes without sign-in appear here. What each area contains is described under The areas of the reference . Open the operation. The title is the short description in German (“Listet Szenen gefiltert und seitenweise”), and below it are the method and path. Read the description. It names the required permission in the form Benötigte Berechtigung: scene.list (“required permission”) and, for public routes, the sentence “Öffentlich — keine Anmeldung erforderlich” (“public, no sign-in required”). If no permission is named, the route needs only authentication. Look at the security section. It states in machine-readable form which credential the route accepts: Entry Credential apiKey API key ak_… oauthAccessToken access token oat_… of a delegated access grant , accepted in the same places as an API key sessionJwt access token of a session eyJ… scimToken SCIM token sct_… , only on the surface under /scim/v2 empty list public Whether a key is also permitted to call the route is decided by the permission from step 3. apiKey in security only means that the credential is accepted. The surface under /scim/v2 names exclusively scimToken , and the "
  },
  {
    "id": "api/read-reference#options",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "Options",
    "anchor": "options",
    "text": "You want How The specification as a file /api/openapi.json on this site (state of the release) or GET /v1/openapi.json on your environment (exactly the running state) Only the operations a key can reach Filter by permission: Key-capable today are asset.* , scene.* , layout-template.* , playlist.* , schedule.* , location.* , the four display permissions fleet.player.list , …read , …update , …command.issue , plus fleet.web-display.manage , advertising.* , analytics.proof.read , and analytics.proof.manage , as well as billing.usage.read and analytics.query.run , see Authentication The errors of an operation Every operation lists 401, 403, 404, 409, 422, 429, and 502 with the seven central responses. The concrete codes are on the error code pages"
  },
  {
    "id": "api/read-reference#check-the-result",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "Every operation you open carries a short description in German, and every schema an example. The call from step 7 answers 200 with items , or with an error body whose code you find on the error code pages."
  },
  {
    "id": "api/read-reference#if-something-goes-wrong",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution The call with the identifier from the example answers 404 The examples are demo data. Insert identifiers from your organization, for example from a list. 403 Permission.OperatorOnly You called a route under /v1/platform/… , which is the operator surface. It is not in this reference. There is nothing for organizations to call there. A route of your environment is missing although it answers It is marked internal , for example GET /health/ready or a route of the operator surface. Its description is not in the public version. In an integration, rely only on what is listed here. POST /v1/webhooks/stripe looks like a webhook endpoint It is the inbound endpoint of the payment provider, not a customer endpoint. You create your own webhooks under Create a webhook endpoint . A route of your environment is missing in the reference Your environment runs a different build than the release of this site. Download GET /v1/openapi.json from your environment."
  },
  {
    "id": "api/read-reference#related",
    "path": "/api/read-reference",
    "area": "api",
    "areaTitle": "API",
    "page": "read-reference",
    "pageTitle": "Read the reference",
    "section": "Related",
    "anchor": "related",
    "text": "The areas of the reference Base URL, /v1, and stability Generate your own client from the specification"
  },
  {
    "id": "api/reference-areas",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "The areas of the reference",
    "anchor": "",
    "text": "The served specification organizes its 443 operations into 19 areas (tags) and 7 groups, and the reference shows them in exactly this order. Read the tables as a map: what an area contains, whether an API key may do anything there, and which command group of the command line corresponds to it. The column With key is derived from the permission catalog: “yes” means the permissions of the area are key-capable. “partial” means some routes need only authentication or a key-capable permission, and the rest need a session. “session” means no permission of the area is intended for keys. “no sign-in” means the route needs no credential. What a key can carry is described under Authentication ."
  },
  {
    "id": "api/reference-areas#identity--access",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Identity & access",
    "anchor": "identity--access",
    "text": "Tag What you find there With key Command line identity User accounts, the user’s own profile, and the means of access of an organization: create, suspend, and reactivate accounts, second factor with recovery codes, API keys, linked provider accounts, plus delegated access with its endpoints outside /v1 and the SCIM surface under /scim/v2 . partial ( GET /v1/me ) user , api-key , invitation , operator sessions All ways into a session: password, sign-in link, platform-wide providers, company sign-in, second factor, organization choice. Plus refreshing and ending, resetting a password, and impersonation by the operator. session (public sign-in routes) whoami access Roles and individual grants, the catalog of all assignable permissions, and the effective permissions of the signed-in account. partial ( GET /v1/permission-catalog , GET /v1/me/permissions ) role , grant , permission onboarding Self-registration with email confirmation, invitations including the public acceptance page, join requests, and the setup assistant. session invitation , self-registration"
  },
  {
    "id": "api/reference-areas#organization--billing",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Organization & billing",
    "anchor": "organization--billing",
    "text": "Tag What you find there With key Command line tenancy The organization itself: master data, business profile, brand, custom domain, and company sign-in with its verified domains. Operators keep the list of organizations and suspend or release them. partial ( GET /v1/me/capabilities ) — (you maintain master data in the portal) billing Book, change, cancel, and reactivate a package. View payment methods and invoices, check usage limits. Operators maintain the article catalog and process invoices and refunds. partial ( GET /v1/me/licensing ) subscription , invoice , catalog"
  },
  {
    "id": "api/reference-areas#content--planning",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Content & planning",
    "anchor": "content--planning",
    "text": "Tag What you find there With key Command line cloud-connections The authorizations for third-party storage accounts from which the media library feeds its places: round trip to consent, state of all connections, disconnecting. Access and refresh tokens never leave the server. session — media-library The media inventory: upload including multipart uploads, folders and dynamic collections, renaming and moving in bulk. For each media item: download links, variants, usage records, trash, and permanent deletion. yes asset , folder composition Create scenes, generate them from templates, publish, archive, and restore. Every change is versioned, versions can be compared and rolled back, and integrity findings can be checked and cleared. yes scene , scene-version , scene-template , layout , widget scheduling What runs when: assemble playlists, create, pause, and resume programs, determine the default scene. Plus calendar view and conflict preview. yes playlist , schedule , display-group , player (program and override) advertising The organization’s advertising, in the stability class beta : advertisers, ad campaigns with their bookings, the reach of a booking ( GET /v1/advertising/booking-lines/{id}/reach ), and the preview of what a booking would deliver ( POST /v1/advertising/booking-lines/preview ). Also creatives from the organization’s own media library with their approval, the ad space per display, the audience per location, participation in the ad network with its parameters, "
  },
  {
    "id": "api/reference-areas#apps--marketplace",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Apps & marketplace",
    "anchor": "apps--marketplace",
    "text": "Tag What you find there With key Command line app-catalog The origin side of apps: Operators fetch releases from the registry, check them, release them (to stable with a second person), demote them, withdraw them, control phase-out periods, and set exceptions per organization. Publicly served are the manifest, bundle, and authoring module of an app version as well as the catalogs of the widgets ( GET /v1/widget-catalog , with the graphics library of the graphic widget under GET /v1/widget-catalog/graphics ) and of the resource types ( GET /v1/resource-type-catalog ) across all apps, plus the versions of an app with their date and what is new in them ( GET /v1/apps/{appId}/history ). partial (public delivery, both catalogs) app-release , app-status marketplace The organization’s view of the app offering: discover, install, update, and uninstall with a preview of the consequences. Deciding on an update: the open decisions ( GET /v1/me/apps/updates ), those of one app ( GET /v1/me/apps/{appId}/update ), accept and reject ( POST …/update/accept , POST …/update/reject ). Opting in to versions from the beta channel, for all apps ( GET|PUT /v1/me/apps/beta-enrollment ) or per app ( POST|DELETE /v1/me/apps/{appId}/beta-enrollment ). Plus the organization’s own inventory, the installed apps ( GET /v1/me/apps ). The store returns its state fully resolved: the overview as cards ( GET /v1/me/apps/store ), the listing of an app ( GET /v1/me/apps/store/{appId} ), the collections ( GET /v1/"
  },
  {
    "id": "api/reference-areas#fleet--playback",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Fleet & playback",
    "anchor": "fleet--playback",
    "text": "Tag What you find there With key Command line fleet The playback devices: commissioning via pairing code, tags, remote commands with feedback, removing displays. Devices report check-ins, capabilities, and the history of their updates. The actions on the device itself (re-pair, factory reset, remove, block) are under identity . The locations also live here, as the folder above the displays, the web displays that run in the browser without a device, and the screenshots of a display as a time series ( GET /v1/players/{playerId}/screenshots ). partial ( location.* , fleet.player.list , …read , …update , …command.issue , fleet.web-display.manage ) device , player , location , playback playback The interface to the renderer: load the playback package, report the start, deliver diagnostic logs. Plus preview of widget data and delivery of the renderer bundle. session (device or operator) playback"
  },
  {
    "id": "api/reference-areas#monitoring--notifications",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Monitoring & notifications",
    "anchor": "monitoring--notifications",
    "text": "Tag What you find there With key Command line audit The tamper-evident audit log: search entries, group operations by their correlation, export extracts. Operators pseudonymize entries with personal data and resolve pseudonyms. session audit notifications Inbox in the portal, personal delivery channels, and webhooks including delivery log, redelivery, and rotation of the secret, plus the event catalog from which a list of subscribed event types is built. session webhook analytics Analyze ad hoc against the metric catalog and output the result as CSV or PDF, along with the overview, the day and the log of a display, the organization’s own content, and the delivery of an ad campaign. Save views, deliver them on a schedule and download them, query metrics, plus the cleanup overview that counts what is left lying around. The location is an axis of the ad hoc analysis. Also the proofs of the bookings with their interim statement, the links for advertisers, and (without sign-in) the verification of a proof and the campaign report behind a link. partial ( analytics.query.run , analytics.proof.read , analytics.proof.manage ) analytics , hygiene , proof"
  },
  {
    "id": "api/reference-areas#platform-operations",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Platform operations",
    "anchor": "platform-operations",
    "text": "An area with two routes without sign-in. The rest of the operator surface is not in this specification (see below). Tag What you find there With key Command line status The two operational routes that anyone may call: the status of the platform as the status page shows it ( GET /v1/public/status ) and the current maintenance window ( GET /v1/public/maintenance-status ). no sign-in health public"
  },
  {
    "id": "api/reference-areas#what-is-not-here-the-operator-surface",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "What is not here: the operator surface",
    "anchor": "what-is-not-here-the-operator-surface",
    "text": "The API also carries around 280 operations under /v1/platform/… : queues, caches, signing keys, permission catalog, event dispatcher, audit log operations. They are the inside view of the house : They are for the platform operator, not for your organization, and a key or a session of an organization gets 403 Permission.OperatorOnly there. These routes are therefore not part of this reference , and the specification at /api/openapi.json does not contain them: A generated client gets only what you may also call. Anyone who operates the platform themselves finds the operator version on their own environment behind a sign-in."
  },
  {
    "id": "api/reference-areas#what-else-the-reference-does-not-show",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "What else the reference does not show",
    "anchor": "what-else-the-reference-does-not-show",
    "text": "Surface Why not The operational routes GET /health/live , GET /health/ready , GET /health/version , and GET /metrics They are marked internal : They describe the state of an environment, not your inventory. They still answer without sign-in, and szenera health queries them. The operation of installed apps and the information about open realtime connections also internal , the inside view of the runtime. Routes that an app brings along ( /v1/apps/<app-id>/… ) Which ones exist depends on the app inventory of your environment. A release state cannot know them. What internal means and what the public version promises instead is described under Stability classes and sunset ."
  },
  {
    "id": "api/reference-areas#related",
    "path": "/api/reference-areas",
    "area": "api",
    "areaTitle": "API",
    "page": "reference-areas",
    "pageTitle": "The areas of the reference",
    "section": "Related",
    "anchor": "related",
    "text": "Read the reference Stability classes and sunset Commands for your organization"
  },
  {
    "id": "api/generate-client",
    "path": "/api/generate-client",
    "area": "api",
    "areaTitle": "API",
    "page": "generate-client",
    "pageTitle": "Generate your own client from the specification",
    "section": "Generate your own client from the specification",
    "anchor": "",
    "text": "There is currently no published client for the Szenera API. This guide shows you how to generate your own from openapi.json and map the conventions of the API into it: credential, errors, version check, idempotency, and paging."
  },
  {
    "id": "api/generate-client#the-honest-status",
    "path": "/api/generate-client",
    "area": "api",
    "areaTitle": "API",
    "page": "generate-client",
    "pageTitle": "Generate your own client from the specification",
    "section": "The honest status",
    "anchor": "the-honest-status",
    "text": "The platform has a TypeScript client, but it is part of the portals and is not published. The developer site does not promise one. What exists is the specification: complete, with an example per schema, in the OpenAPI 3.1.0 format. Any common generator can produce a client from it."
  },
  {
    "id": "api/generate-client#steps",
    "path": "/api/generate-client",
    "area": "api",
    "areaTitle": "API",
    "page": "generate-client",
    "pageTitle": "Generate your own client from the specification",
    "section": "Steps",
    "anchor": "steps",
    "text": "Download the specification: Or download it from your environment if its version differs from the release: curl -sS -o openapi.json \"$SZENERA_API/v1/openapi.json\" . Generate the client with a generator of your choice. First check that it understands OpenAPI 3.1 : The specification uses types such as [\"string\", \"null\"] , which a 3.0 generator does not know. The 19 tags of the specification work well as module boundaries. Set the credential centrally: Authorization: Bearer ak_… on every call, taken from an environment variable and never from the source code. A key does not expire after 15 minutes, so your client needs no refresh. Map errors to code . Every error has the form Area.Cause . Derive one typed error per namespace ( Auth.* , Permission.* , Validation.* , Conflict.* , RateLimit.* , *.NotFound , Integration.* ) and carry traceId in each of them. Never branch on message . The format is described under The error format . Map the version check: For scenes, playlists, programs, roles, and displays, read the version field and send it as If-Match with every change. On 409 Conflict.OptimisticLock , your client reads again and retries once. See Concurrent changes . Give every modifying call an Idempotency-Key : self-chosen, at least 8 characters, new for each action. A repeat with the same key returns the stored response with Idempotent-Replay: true , and the same key with a different body answers 422 Validation.IdempotencyKeyReused . Details are described under Idempotency from"
  },
  {
    "id": "api/generate-client#check-the-result",
    "path": "/api/generate-client",
    "area": "api",
    "areaTitle": "API",
    "page": "generate-client",
    "pageTitle": "Generate your own client from the specification",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "The schema examples are your first test data: all 741 schemas of the served specification carry one, and they fit together (same identifiers, same organization). A first read call, GET /v1/me , answers 200 with the tenantId of your organization. A call with a wrong key lands in your typed Auth error with code: Auth.ApiKeyUnknown . Every operation your client offers carries x-stability: stable or beta in the specification (with beta , the shape can still move). You compare a later version of the specification with Compare the specification against your version ."
  },
  {
    "id": "api/generate-client#if-something-goes-wrong",
    "path": "/api/generate-client",
    "area": "api",
    "areaTitle": "API",
    "page": "generate-client",
    "pageTitle": "Generate your own client from the specification",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution The generator aborts with a format error It does not know OpenAPI 3.1. Choose a generator with 3.1 support. 422 Validation.Failed with details.issues The body violates the schema, and issues names the field path. Compare the example of the request schema with your body. 403 Permission.Denied on a route that the client offers The generator produces every operation, including those a key may not call. Check the permission in the reference. Permissions that are not key-capable stay reserved for sessions, see Authentication . A field the client requires is missing from the response Your environment runs a different build than the specification you generated from. Download the specification of the environment ( GET /v1/openapi.json ) and generate again."
  },
  {
    "id": "api/generate-client#related",
    "path": "/api/generate-client",
    "area": "api",
    "areaTitle": "API",
    "page": "generate-client",
    "pageTitle": "Generate your own client from the specification",
    "section": "Related",
    "anchor": "related",
    "text": "Read the reference The error format Compare the specification against your version"
  },
  {
    "id": "cli/index",
    "path": "/cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "index",
    "pageTitle": "CLI",
    "section": "CLI",
    "anchor": "",
    "text": "The szenera command line: profiles, credentials, 136 commands, JSON output and exit codes, and what applies today when you obtain it."
  },
  {
    "id": "cli/index#set-up",
    "path": "/cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "index",
    "pageTitle": "CLI",
    "section": "Set up",
    "anchor": "set-up",
    "text": "Page When you need it Get the CLI How to get the CLI today: no npm package, no download. The CLI is built from the platform’s source tree, and without access you are left with the API. Profiles and credentials Create a profile with an environment, a credential type, and write protection, either as a file or with environment variables only, and check it with whoami."
  },
  {
    "id": "cli/index#commands",
    "path": "/cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "index",
    "pageTitle": "CLI",
    "section": "Commands",
    "anchor": "commands",
    "text": "Page When you need it Commands for your organization All szenera commands that run with a key or account of your organization: media, scenes, playlists, programs, devices, team, apps, package. Commands for platform operations The commands of the internal szenera-ops tool that run only with an operator profile: device software, approvals, incidents, queues, organizations, operator accounts."
  },
  {
    "id": "cli/index#in-scripts",
    "path": "/cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "index",
    "pageTitle": "CLI",
    "section": "In scripts",
    "anchor": "in-scripts",
    "text": "Page When you need it JSON output and exit codes JSON without an envelope on standard output, errors only on standard error, --raw for contract violations, and the 13 exit codes from 0 to 12. Scripts and CI Work without a profile file by using environment variables, evaluate exit codes, fetch lists completely, limit wait times, and make retries safe. When the CLI aborts Unknown command, profile blocked, 401 despite a key, response contradicts the contract, exit 11 or 12: symptom, meaning, solution."
  },
  {
    "id": "cli/get-cli",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "Get the CLI",
    "anchor": "",
    "text": "After this guide, the szenera command line is built on your machine, and szenera --help shows all commands. First you learn which ways of getting it exist today, and which do not."
  },
  {
    "id": "cli/get-cli#the-current-state",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "The current state",
    "anchor": "the-current-state",
    "text": "The command line is the package @szenera/cli in the platform’s source tree. It carries version 0.1.0 , is not published on npm, and is not attached to any release as a download. The platform’s release flows produce container images and device software, but no CLI artifact. If you have the source tree, you build the CLI from it. If you do not, you work with curl against the same routes: every command is an API call, and the command pages name the route and the permission each command needs. This page says nothing about a future publication. It describes what applies today. The package contains the commands for organizations. The commands for platform operations belong to a separate, internal tool and are not part of szenera ."
  },
  {
    "id": "cli/get-cli#steps",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "Steps",
    "anchor": "steps",
    "text": "Install the source tree’s dependencies completely, never for a single sub-package only: Build the package. The build creates dist/ including the executable file dist/main.js : Call the built file directly. Without a command and with --help , it shows the overview of all commands from the registry: Optionally link the file as szenera so the examples on the other pages apply literally. The package declares szenera as the program name for dist/main.js , and a symlink in a directory on your search path is enough:"
  },
  {
    "id": "cli/get-cli#options",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "Options",
    "anchor": "options",
    "text": "Two call forms at a glance: If you evaluate the exit codes in a script or in CI, call the file directly and never through pnpm run : pnpm run swallows exit codes and reports every failure as 1 ."
  },
  {
    "id": "cli/get-cli#check-the-result",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "szenera --help (or node packages/cli/dist/main.js --help ) lists 136 commands in groups such as asset , scene , device , api and ends with the block Überall gültig (“valid everywhere”: --help , --profile=<name> , --json , --table , --raw ). szenera <befehl> --help shows the positional arguments, flags, and examples of a command. Both come from the registry, not from maintained text. --help never signs in and needs no profile. In the API’s session list, the CLI later identifies itself as szenera-cli/0.1.0 . That is the version that also appears in error messages about the contract."
  },
  {
    "id": "cli/get-cli#if-something-goes-wrong",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution node packages/cli/dist/main.js reports that the file does not exist The build is missing. dist/ only exists after pnpm --filter @szenera/cli build . Run step 2. The call aborts with a syntax or module error before any command runs The Node version is too old. The source tree requires Node 24 or later. Use Node 24 or newer, then rebuild. A script sees exit 1 for every error The call runs through pnpm run , which swallows the codes. Call the file directly through node . See JSON output and exit codes . szenera health ends with Keine Konfiguration gefunden (no configuration found) The CLI runs but does not know which environment to talk to. Create a profile or set SZENERA_BASE_URL . See Profiles and credentials . szenera does not know a command such as tenant list or incident create That is a platform operations command, and it does not belong to szenera . See Commands for platform operations ."
  },
  {
    "id": "cli/get-cli#related",
    "path": "/cli/get-cli",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "get-cli",
    "pageTitle": "Get the CLI",
    "section": "Related",
    "anchor": "related",
    "text": "Profiles and credentials Your first call with curl Commands for your organization"
  },
  {
    "id": "cli/profiles-and-credentials",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Profiles and credentials",
    "anchor": "",
    "text": "After this guide, the CLI knows your environment, your credential, and the write protection, as a profile in a file or through environment variables only, and szenera whoami shows which account and organization it talks to. A profile bundles two things: the credential and the mapping from service to base URL. Everything a command needs to know about the other end is stored there and nowhere else. Every profile belongs to one organization."
  },
  {
    "id": "cli/profiles-and-credentials#steps",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Steps",
    "anchor": "steps",
    "text": "Create the profile file ~/.config/szenera/profile.json ( $XDG_CONFIG_HOME is respected if it is set). Its structure: Enter the base URL under services . The * entry applies to everything without its own entry and is enough today. The address must begin with http:// or https:// , and a trailing slash is removed. Choose the credential under credential . The type api-key with a value ak_… under key is the standard way. The other two types are described under “Options”. Decide on readOnly . true blocks every command that changes something before a single network packet is created, so keep it for a production profile. If the setting is missing, true applies. Set default to the profile that should apply without --profile= . If the file holds exactly one profile, that one applies even without a default. If it holds several without a default, the CLI aborts and names them. Check the profile:"
  },
  {
    "id": "cli/profiles-and-credentials#options",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Options",
    "anchor": "options",
    "text": ""
  },
  {
    "id": "cli/profiles-and-credentials#without-a-file-environment-variables",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Without a file: environment variables",
    "anchor": "without-a-file-environment-variables",
    "text": "Environment variables sit above the file: every variable that is set replaces the matching value of the chosen profile. SZENERA_BASE_URL and SZENERA_API_KEY alone carry a complete profile, with no file at all. It is then named environment and is not write-protected. Variable Effect SZENERA_PROFILE Which profile of the file applies. --profile=<name> takes precedence SZENERA_BASE_URL Sets the * entry under services SZENERA_API_KEY API key. Replaces the profile’s credential SZENERA_OUTPUT json or table . --json and --table take precedence SZENERA_PROFILE_FILE Path of the profile file. Takes precedence over any search"
  },
  {
    "id": "cli/profiles-and-credentials#where-the-cli-looks",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Where the CLI looks",
    "anchor": "where-the-cli-looks",
    "text": "The first hit wins: Location When $SZENERA_PROFILE_FILE explicitly named ~/.config/szenera/profile.json otherwise. $XDG_CONFIG_HOME is respected"
  },
  {
    "id": "cli/profiles-and-credentials#credential-types",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Credential types",
    "anchor": "credential-types",
    "text": "api-key : the standard way. The key travels as Authorization: Bearer with every call and does not expire after a quarter of an hour, so the CLI needs neither a cache nor a refresh for it. A value without a prefix that the API recognizes as a key is rejected before a network packet is created, as soon as a command needs the credential (exit 2). --help and health run even with a wrong value. password : for the person at the computer. The profile carries email and password and, with two-factor sign-in, additionally totpSecret (Base32). The CLI computes the one-time code itself. The access token is then stored with permissions 0600 next to the configuration ( .szenera-token-<profil>.json ), is discarded one minute before it expires, and is renewed before every call if it will not last until the next round trip, even in the middle of a command that waits for minutes. If the account belongs to several organizations, the sign-in aborts instead of guessing. none : public routes only. szenera health runs with it. Every other command aborts with a note that the profile carries no credential."
  },
  {
    "id": "cli/profiles-and-credentials#services-per-domain",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Services per domain",
    "anchor": "services-per-domain",
    "text": "Instead of * , an entry under services may name a platform domain (such as fleet or composition ) and give it its own address. * remains for the rest. A name that is not a domain is rejected when the file is read, so a typo does not silently fall back to * ."
  },
  {
    "id": "cli/profiles-and-credentials#check-the-result",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "szenera whoami shows the ID, email, name, Organization , and the number of roles (route GET /v1/me ). If a different organization appears than the one you expected, the profile points somewhere other than you intended. It is not the key that is wrong, but the choice."
  },
  {
    "id": "cli/profiles-and-credentials#if-something-goes-wrong",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution Keine Konfiguration gefunden — die Kommandozeile weiß nicht, mit welcher Umgebung sie sprechen soll. (no configuration found) (exit 2) Neither a file at one of the two locations nor SZENERA_BASE_URL . Create the file or set the two variables. Unbekanntes Profil „…\". … führt: dev, prod. (unknown profile) (exit 2) --profile= or SZENERA_PROFILE names a profile the file does not know. Choose one of the names listed. … ist im Profil „prod\" gesperrt — das Profil ist schreibgeschützt. (blocked, the profile is write-protected) (exit 2) A command that changes something meets a write-protected profile, or one without the setting. Set readOnly: false if the profile may write. … „audience\" muss „tenant\" sein. (audience must be tenant) (exit 2) The file holds a profile for platform operations. szenera does not know it. Remove the profile from the file or name a separate file through SZENERA_PROFILE_FILE . Kein API-Schlüssel: erwartet wird ein Wert mit dem Präfix … (not an API key) (exit 2) The value in the profile or in SZENERA_API_KEY is not a key. Take the plaintext from the portal. See Create an API key . … braucht einen Ausweis, das Profil „…\" trägt keinen. (needs a credential, the profile has none) (exit 1) The profile has no credential or has kind: \"none\" . Add SZENERA_API_KEY=ak_… or set credential in the profile. HTTP 401 with exit 3 The key is not valid: revoked, expired, or the profile points to a different environment than the key. Check each profile wi"
  },
  {
    "id": "cli/profiles-and-credentials#related",
    "path": "/cli/profiles-and-credentials",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "profiles-and-credentials",
    "pageTitle": "Profiles and credentials",
    "section": "Related",
    "anchor": "related",
    "text": "Get the CLI JSON output and exit codes Create an API key"
  },
  {
    "id": "cli/commands-organization",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Commands for your organization",
    "anchor": "",
    "text": "All commands of the szenera command line. They run with an API key ak_… or an account of your organization. Each table names, per command, the route, the permission required, whether an API key may carry the permission (“Key”), and whether the command changes something. The “Key” column follows the platform’s permission catalog: a permission reserved for accounts is never granted to a key. The command then answers with 403 (exit 4), and you need a profile with password sign-in. szenera <befehl> --help shows the positional arguments, flags, and examples of every command. The description there is the source of these tables. This page follows the registry of szenera with 136 commands, and all of them are listed here. 21 of them also serve platform operations and partly take a different route there. Those routes, and the commands that serve operations only, are described under Commands for platform operations ."
  },
  {
    "id": "cli/commands-organization#flags-valid-everywhere",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Flags valid everywhere",
    "anchor": "flags-valid-everywhere",
    "text": "Flag Effect --help Show help for the command without doing anything. Never signs in --profile=<name> Which profile applies (environment, credential, service addresses) --json Force output as JSON --table Force output as a table --raw If a response breaks the contract, print the raw response A flag carries its value after the equals sign ( --limit=20 ), and --limit 20 does not exist. An unknown flag is an error (exit 1), not a silent hint. List commands page through results with --limit=<anzahl> (1 to 200, up to 100 for filtered lists), --cursor=<marke> , and --all (follow all pages). Commands that wait for an operation accept --timeout=<dauer> ( 90s , 20m , 2h , or a number in seconds) and some also --interval=<dauer> and --no-wait . Irreversible actions require --yes . Without the flag, the command aborts with exit 1 and says what would happen."
  },
  {
    "id": "cli/commands-organization#environment-and-credential",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Environment and credential",
    "anchor": "environment-and-credential",
    "text": "Command Purpose Route and permission Key Modifies health Whether the environment reports ready, and which check says what GET /health/ready , no credential needed, exit 9 on 503 — no health public What the status page shows publicly: state per component, maintenance windows, public incidents GET /v1/public/status , no credential needed — no whoami Who your own credential is: account, organization, permissions GET /v1/me , sign-in only, no permission of its own yes no hygiene get What has been left lying around: six numbers, and for each number the command that shows the rows behind it GET /v1/analytics/hygiene · analytics.query.run . --offline-since (default P7D ) and --dormant-since (default P90D ) set the two thresholds yes no"
  },
  {
    "id": "cli/commands-organization#media-and-folders",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Media and folders",
    "anchor": "media-and-folders",
    "text": "Command Purpose Route and permission Key Modifies asset list The media of the media library. --usage=unused finds what is in no scene, no playlist, and no brand mark GET /v1/assets · asset.list yes no asset get <ast_…> One media item with its variants. --usages names where it is used GET /v1/assets/{id} , GET /v1/assets/{id}/usage · asset.read yes no asset upload --file=<pfad> Upload a file to the media library and wait until it is usable POST /v1/assets/upload-precheck → bytes → POST /v1/assets/upload-complete · asset.upload . Waits until ready , default --timeout=10m , --no-wait yes yes asset download <ast_…> --output=<pfad> Fetch a media item or one of its variants POST /v1/assets/{id}/download-url (variant: /variant-url ) → bytes · asset.read yes no asset update <ast_…> Change the name, tags, or folder of a media item PATCH /v1/assets/{id} · asset.update yes yes asset reprocess <ast_…> Regenerate the variants of a media item and wait POST /v1/assets/{id}/reprocess · asset.update . Waits as with uploading yes yes asset delete <ast_…> --yes Move a media item to the trash DELETE /v1/assets/{id} · asset.delete yes yes asset restore <ast_…> Bring a media item back from the trash POST /v1/assets/{id}/restore · asset.update yes yes asset purge <ast_…> --yes Delete a media item permanently POST /v1/assets/{id}/purge · asset.delete yes yes asset bulk-update --assets=<ast_…,…> Move or tag many media items (up to 100) POST /v1/assets/bulk-move or POST /v1/assets/bulk-tag · asset.upd"
  },
  {
    "id": "cli/commands-organization#scenes-versions-and-layouts",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Scenes, versions, and layouts",
    "anchor": "scenes-versions-and-layouts",
    "text": "Command Purpose Route and permission Key Modifies scene list The organization’s scenes. --assignment=none finds those that no program, no schedule, and no assigned playlist points to (useful with --status=published ) GET /v1/scenes · scene.list yes no scene get <scn_…> A scene with its open findings GET /v1/scenes/{id} , GET /v1/scenes/{id}/integrity-issues · scene.read yes no scene create --name=<name> Create an empty scene POST /v1/scenes · scene.create yes yes scene create-from-template --template=<sct_…> --name=<name> Create a scene from a curated template POST /v1/scenes/from-template · scene.create yes yes scene update <scn_…> Change the draft of a scene. Without --version , the command reads the version beforehand PATCH /v1/scenes/{id} · scene.update yes yes scene publish <scn_…> Publish the draft as a new version. Names findings instead of swallowing them POST /v1/scenes/{id}/publish · scene.publish yes yes scene recheck <scn_…> Re-check the integrity and name the findings POST /v1/scenes/{id}/integrity-issues/recheck · scene.read yes yes scene resolve-issue <sii_…> --scene=<scn_…> --resolution=manual-fixed|ignored Close a finding as fixed or ignored POST /v1/scenes/{id}/integrity-issues/{iid}/resolve · scene.update yes yes scene archive <scn_…> Archive a scene POST /v1/scenes/{id}/archive · scene.archive yes yes scene restore <scn_…> Bring back an archived or deleted scene POST /v1/scenes/{id}/restore · scene.update yes yes scene delete <scn_…> --yes Move a scene to "
  },
  {
    "id": "cli/commands-organization#playlists-and-programs",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Playlists and programs",
    "anchor": "playlists-and-programs",
    "text": "Command Purpose Route and permission Key Modifies playlist list The organization’s playlists GET /v1/playlists · playlist.list yes no playlist get <pll_…> A playlist with its items GET /v1/playlists/{id} · playlist.read yes no playlist create --name=<name> Create a playlist: --items (sequence) or --folder / --tags (rule) POST /v1/playlists · playlist.create yes yes playlist update <pll_…> Change a playlist. --items replaces the whole sequence PATCH /v1/playlists/{id} · playlist.update yes yes playlist delete <pll_…> --yes Delete a playlist. The API refuses assigned ones DELETE /v1/playlists/{id} · playlist.delete yes yes playlist preview-source Resolve an automatic source on a trial basis POST /v1/playlists/source-preview · playlist.read and asset.read yes no schedule list The organization’s programs GET /v1/schedules · schedule.list yes no schedule get <sch_…> A program with its entries. --players names the devices that play it GET /v1/schedules/{id} , GET /v1/schedules/{id}/players · schedule.read yes no schedule create --name=<name> Create a program. Without --base , its base is off air POST /v1/schedules · schedule.create yes yes schedule update <sch_…> Change the name or base of a program PATCH /v1/schedules/{id} · schedule.update yes yes schedule delete <sch_…> --yes Delete a program. The API refuses assigned ones DELETE /v1/schedules/{id} · schedule.delete yes yes schedule create-item --schedule=<sch_…> --content=… --recurrence=… Create a program entry ( once: , daily:"
  },
  {
    "id": "cli/commands-organization#playback-and-widgets",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Playback and widgets",
    "anchor": "playback-and-widgets",
    "text": "Command Purpose Route and permission Key Modifies playback now What is playing on all displays right now ( play , pause , gap ) GET /v1/players/now-playing · fleet.player.list yes no playback timeline <ply_…> The resolved playback timeline of a display (window from now, 4 hours to 7 days) GET /v1/players/{id}/timeline · fleet.player.read yes no playback history <ply_…> --date=<datum> The recorded daily history of a display (retained for 90 days) GET /v1/players/{id}/playback-history · fleet.playback-history.read no no playback refresh <ply_…> Recompute the snapshot of a display POST /v1/players/{id}/snapshot/refresh · fleet.player.update yes yes widget preview --widget=<pfad> Resolve a widget’s data for a preview POST /v1/widget-data/preview · scene.read yes no"
  },
  {
    "id": "cli/commands-organization#advertising",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Advertising",
    "anchor": "advertising",
    "text": "Advertisers, ad campaigns, bookings, and creatives of your own advertising business. Reading works in every package. Creating and approving additionally require the advertisingDirect capability of the package. Without it, the server answers with 403 and Billing.FeatureNotEntitled (exit 4). Ad campaigns of the ad network appear in campaign list as soon as they reach displays of your organization, without price and without the operator’s note. Command Purpose Route and permission Key Modifies advertiser list The advertisers, filterable by --status , --industry , and --search GET /v1/advertising/advertisers · advertising.advertiser.read yes no advertiser get <adv_…> One advertiser with brands, contacts, and note GET /v1/advertising/advertisers/{id} · advertising.advertiser.read yes no advertiser create --name=<name> --industry=<branche> Create an advertiser. --brand may be repeated POST /v1/advertising/advertisers · advertising.advertiser.manage yes yes campaign list The ad campaigns with phase, period, number of bookings, and displays reached. --owner=tenant or --owner=network separates your own business from the network GET /v1/advertising/campaigns · advertising.campaign.read yes no campaign get <acp_…> One ad campaign with phase, period, goal, and price GET /v1/advertising/campaigns/{id} · advertising.campaign.read yes no booking list --campaign=<acp_…> The bookings of an ad campaign with spot length, frequency, and commitment GET /v1/advertising/campaigns/{id}/booking-lines"
  },
  {
    "id": "cli/commands-organization#devices",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Devices",
    "anchor": "devices",
    "text": "Command Purpose Route and permission Key Modifies player list The organization’s displays. --program=none finds those without content, --offline-since=P7D those that have reported nothing for that long (one that never reported counts too) GET /v1/players · fleet.player.list , plus --status , --search yes no device list The devices: what is out in the field, and does it still check in GET /v1/devices · identity.device.list no no device get <dev_…> One device in detail: certificate and surfaces GET /v1/devices/{id} · identity.device.list no no device send-command <dev_…> <kind> Issue a remote maintenance command to a device ( reboot , locate , tail-log , mark-good , check-ota ) or to a surface ( screenshot , identify , set-config , display-power , display-source , refresh-snapshot , with --screen=<n> ). A 201 response means accepted, and --wait stays attached POST /v1/devices/{id}/commands , POST /v1/players/{id}/commands · fleet.player.command.issue . Surface kinds also resolve through identity.device.list . That permission is not key-capable, so only the machine kinds run with a key yes yes device list-commands <dev_…> The latest remote maintenance commands of a device GET /v1/devices/{id}/commands · fleet.player.read yes no device screenshot <dev_…> Request a screenshot, wait for it, and fetch it ( --file ) POST /v1/players/{id}/commands → GET …/commands/{cid}/output · fleet.player.command.issue , fleet.player.read , and identity.device.list . The last one is not key-capable"
  },
  {
    "id": "cli/commands-organization#locations",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Locations",
    "anchor": "locations",
    "text": "Command Purpose Route and permission Key Modifies location list The organization’s locations. The last column counts the displays at the location GET /v1/locations · location.list , complete, without paging yes no location get <loc_…> A location with address, time zone, position, note, and version GET /v1/locations/{id} · location.read yes no location create --name=<name> --timezone=<IANA> Create a location. The address comes through --street , --postal-code , --city , --country , the position through --latitude / --longitude , plus --note POST /v1/locations · location.create , needs the Locations capability yes yes location update <loc_…> Change the name, time zone, address, position, or note. --clear-address and --clear-coordinates remove the value PATCH /v1/locations/{id} · location.update yes yes location delete <loc_…> --yes Delete an empty location. With displays attached, the API answers 409 Location.HasPlayers DELETE /v1/locations/{id} · location.delete yes yes player assign-location <ply_…> --location=<loc_…> Attach a display to a location. A second call moves it instead of duplicating it PATCH /v1/players/{id} · fleet.player.update yes yes player unassign-location <ply_…> Detach a display from its location. It keeps its program, settings, and license PATCH /v1/players/{id} · fleet.player.update yes yes A location is a folder for displays: it carries no permissions and no inventory of its own, and deleting it never takes a display with it. The fields and error codes "
  },
  {
    "id": "cli/commands-organization#analytics",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Analytics",
    "anchor": "analytics",
    "text": "Command Purpose Route and permission Key Modifies analytics overview The overview: per display the availability within the planned time, and above it gaps, planned hours reported on time, and plays. --location (repeatable) restricts it to locations GET /v1/analytics/overview · analytics.query.run yes no analytics explore --measure=<kennzahl> Analyze metrics ad hoc: --measure and --by are repeatable, plus --sort , --ascending , --limit , --perspective , and --granularity . --query=<pfad> takes the complete query, including filters, as JSON POST /v1/analytics/explore · analytics.query.run yes no analytics delivery <acp_…> The delivery of an ad campaign: per booking the commitment, what was delivered, fulfillment, pacing, forecast with range, and the finding GET /v1/analytics/campaigns/{id}/delivery · advertising.campaign.read yes no Without --from and --to , the last seven days up to yesterday apply. Without a window, an ad campaign shows its run up to yesterday. The table gives hours instead of seconds and percent instead of share, and next to a value stands its upper bound as long as open gaps can still be reported late. Which metric can be calculated with which dimension is shown by szenera api get /v1/analytics/catalog ."
  },
  {
    "id": "cli/commands-organization#proofs",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Proofs",
    "anchor": "proofs",
    "text": "Command Purpose Route and permission Key Modifies proof list The organization’s proofs, newest first: closings, addenda, and interim statements with number, period, ad campaign, booking, valid plays, and fulfillment. --campaign , --advertiser , --line , --kind , --from , and --to narrow the list GET /v1/analytics/proofs · analytics.proof.read yes no proof show <prf_…> One proof with check code, signature, and its files including checksum. --json shows the figures, the addenda, and the status of each delivery GET /v1/analytics/proofs/{id} · analytics.proof.read yes no proof download <prf_…> --output=<pfad> Fetch a file: --kind=pdf (default), csv , manifest , or capture with --file=<pfi_…> . If the checksum does not match, the file is removed again GET /v1/analytics/proofs/{id}/files/{kind} · analytics.proof.read yes no With the number and check code from proof show , anyone can verify a proof on the verification page, even without an account. Interim statements and links for advertisers are created in the portal or through the API."
  },
  {
    "id": "cli/commands-organization#event-targets",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Event targets",
    "anchor": "event-targets",
    "text": "Command Purpose Route and permission Key Modifies webhook list The organization’s event targets. --status=dead shows those that delivery itself gave up on (disabled and with an error chain longer than twenty) GET /v1/webhooks · webhook.list no no A target that was switched off by hand is under --status=disabled and is something different. Targets are created and changed in the portal or through the API. See Create a webhook endpoint ."
  },
  {
    "id": "cli/commands-organization#team-roles-and-grants",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Team, roles, and grants",
    "anchor": "team-roles-and-grants",
    "text": "Command Purpose Route and permission Key Modifies user list The organization’s members. --last-login-before=<zeitpunkt mit Zone> finds dormant accounts, including ones that never signed in GET /v1/users · identity.user.list no no user get <usr_…> One member in detail GET /v1/users/{id} · identity.user.read no no user create --email=… --name=… --role=<rol_…> Create a member directly (without an invitation), with --password or --federated POST /v1/users · identity.user.create no yes user update <usr_…> Change a member’s profile PATCH /v1/users/{id} · identity.user.update no yes user set-role <usr_…> --role=<rol_…> --reason=… Set the role of a member. It replaces the previous one PATCH /v1/users/{id}/roles · identity.user.assign-roles no yes user suspend <usr_…> --reason=… Suspend a member POST /v1/users/{id}/suspend · identity.user.suspend no yes user reactivate <usr_…> Lift the suspension of a member POST /v1/users/{id}/reactivate · identity.user.reactivate no yes user delete <usr_…> --reason=<kategorie> --yes Delete a member. The reason is one of the API’s five categories DELETE /v1/users/{id} · identity.user.delete no yes invitation list The organization’s invitations, with --pending only the open ones GET /v1/invitations · identity.invitation.list no no invitation create --email=… --role=<rol_…> Invite someone to the organization POST /v1/invitations · identity.user.invite no yes invitation resend <ivt_…> Send an invitation again. That extends the deadline POST /v1/invitati"
  },
  {
    "id": "cli/commands-organization#keys",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Keys",
    "anchor": "keys",
    "text": "Command Purpose Route and permission Key Modifies api-key list Your own access keys, with --organization all keys of the organization GET /v1/me/api-keys (session only), with --organization GET /v1/api-keys · identity.api-key.list no no api-key issue --name=… --permissions=<a,b,c> Issue a key. The secret value appears exactly once. --list-allowed shows the permissions that are allowed POST /v1/me/api-keys · identity.api-key.create no yes api-key rotate <akm_…> Renew a key. The old one stays valid for a grace period (0–72 hours) POST /v1/me/api-keys/{id}/rotate · identity.api-key.create no yes api-key revoke <akm_…> --yes Revoke a key, immediately and permanently. --compromised records in the log the suspicion that the secret value fell into the wrong hands DELETE /v1/me/api-keys/{id} · identity.api-key.revoke no yes A key can never do more than its issuer, and it can never carry a permission that is not allowed for keys. The CLI checks the second condition before the call."
  },
  {
    "id": "cli/commands-organization#package-invoices-and-apps",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Package, invoices, and apps",
    "anchor": "package-invoices-and-apps",
    "text": "Command Purpose Route and permission Key Modifies subscription get The package and the licensing status of your own organization GET /v1/me/subscription · billing.subscription.read , GET /v1/me/licensing · billing.usage.read no no subscription set-licenses --count=<n> Change the number of booked licenses, but not below the number in use PUT /v1/me/licensing/licenses · billing.subscription.update no yes subscription cancel --yes Cancel the contract: at the end of the period, or immediately with --immediately POST /v1/me/subscription/cancel · billing.subscription.update no yes subscription reactivate Withdraw a cancellation that is already scheduled POST /v1/me/subscription/reactivate · billing.subscription.update no yes app subscribe <app> Book a priced app for billing POST /v1/me/licensing/apps/{appId} · billing.subscription.update no yes app unsubscribe <app> --yes Cancel a booked app, effective at the end of the period DELETE /v1/me/licensing/apps/{appId} · billing.subscription.update no yes invoice list Your own invoices GET /v1/me/invoices · billing.invoice.read no no invoice get <inv_…> An invoice with its line items. --pdf fetches the address of the PDF GET /v1/me/invoices/{id} · billing.invoice.download no no app list The apps in the store, with --installed only the installed ones GET /v1/me/apps/store , no permission of its own yes no app get <app> One app in detail: permissions, data access, widgets GET /v1/me/apps/store/{appId} , no permission of its own, session on"
  },
  {
    "id": "cli/commands-organization#log-permission-catalog-and-escape-hatch",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Log, permission catalog, and escape hatch",
    "anchor": "log-permission-catalog-and-escape-hatch",
    "text": "Command Purpose Route and permission Key Modifies audit list Read the audit log: append-only, payloads redacted GET /v1/audit · audit.read or audit.read.self (then only your own operations) no no audit get <aud_…> One log entry with its payload GET /v1/audit/{id} · audit.read or audit.read.self no no permission catalog The permission catalog as the running environment knows it, with --key-eligible the allowlist GET /v1/permission-catalog , sign-in only n/a no api get <path> A GET on any path of the API the route of the path and its permission per route no api post <path> <body> A POST on any path, plus --idempotency-key=<schluessel> the route of the path and its permission per route yes api put <path> <body> A PUT on any path, plus --idempotency-key=<schluessel> the route of the path and its permission per route yes api patch <path> <body> A PATCH on any path, plus --idempotency-key=<schluessel> the route of the path and its permission per route yes api delete <path> [body] A DELETE on any path, plus --idempotency-key=<schluessel> the route of the path and its permission per route yes api … is the escape hatch to every route that has no command of its own. The path begins with a slash and may carry a query. The body is JSON directly or @datei.json . This way also goes through the generated client, so headers, trace ID, and error kinds apply as everywhere, but it does not check the response against a contract: no one knows which schema belongs to a freely typed path."
  },
  {
    "id": "cli/commands-organization#related",
    "path": "/cli/commands-organization",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-organization",
    "pageTitle": "Commands for your organization",
    "section": "Related",
    "anchor": "related",
    "text": "Commands for platform operations JSON output and exit codes The areas of the reference"
  },
  {
    "id": "cli/commands-platform-operations",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Commands for platform operations",
    "anchor": "",
    "text": "The commands of the szenera-ops tool that run only with a profile of the audience operator : an operator account or an operator key opk_… . They do not concern customers of the platform and are not part of the szenera command line. They are listed here because whoami , device , audit , player , playback , permission , api-key , and invoice serve both sides, depending on the profile. The tool lives in the source tree under tools/ops-cli and is not shipped. It runs with pnpm -s szenera-ops <befehl> and carries all 230 commands of the platform. Profiles, environment variables, output, and exit codes are the same as for szenera . In addition, it knows the audience operator (profile field audience , environment variable SZENERA_AUDIENCE ), its own profile file .secrets/szenera-profile.json in the source tree, and the fallback to the environments in .secrets/umgebungen.json . A command that serves only one side rejects the profile of the other before a call goes out. The tables have the same form as under Commands for your organization : command, purpose, route and permission, whether a key may carry the permission, and whether the command changes something. Operator keys are valid for 1 to 365 days (default 90). Which permissions such a key may carry at all is shown by szenera-ops permission catalog --key-eligible under an operator profile. They are the read-only operations routes, the device software, and reading ad sales. Everything else stays reserved for people with two-factor"
  },
  {
    "id": "cli/commands-platform-operations#health",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Health",
    "anchor": "health",
    "text": "Command Purpose Route and permission Key Modifies health deep A verdict from five sources, the command to run after a deployment. One red source makes the command red, a silent one appears as unknown GET /health/ready , GET /v1/platform/ops/platform-health , GET …/ops/jobs/health , GET …/ops/cache/health , GET …/ops/events/dispatcher/health , GET …/ops/audit/health · platform.ops.health.read , platform.jobs.health.read , platform.cache.stats.read , platform.events.dispatcher.read , platform.audit.health.read partly: cache and audit log only with an account no With --json , the response carries the verdict under verdict ( green or yellow ) and, per source, the fields name , status , and hint under sources . A red verdict ends with exit 11 instead. The counter-check from outside is health public : the same information the status page shows, without a credential and without an operator profile. It answers “What do our customers see right now?” and not “How are things really?”. The command is listed under Commands for your organization ."
  },
  {
    "id": "cli/commands-platform-operations#fleet-devices",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Fleet devices",
    "anchor": "fleet-devices",
    "text": "Command Purpose Route and permission Key Modifies device list The devices of all organizations, including those without an organization. Plus --tenant=<tnt_…> , --all-tenants , --silent-only (silent for over a quarter of an hour) GET /v1/platform/ops/devices · platform.ops.device.read yes no device get <dev_…> One device in the operations view: organization and serial number GET /v1/platform/ops/devices/{deviceId} · platform.ops.device.read yes no device lookup --serial=<text> Find a device by its hardware serial number GET /v1/platform/ops/devices/lookup · platform.ops.device.read yes no device get-command <dev_…> <cmd_…> What became of a command sent to a device. --wait waits until the end (exit 11 on failed / timeout , exit 12 on the time limit) GET /v1/platform/ops/devices/{id}/commands/{commandId} · platform.ops.device.command-read yes no device release <dev_…> --reason=reconnect|factory|removed --yes Release a device. The reason is mandatory. reconnect and removed leave Wi-Fi and remote access in place. factory takes both along. All displays of the device go with it POST /v1/platform/ops/devices/{deviceId}/release · platform.ops.device.reset no yes device block <dev_…> --reason=<text> --yes Block a device: it loses its organization, all displays go with it, every certificate is revoked, and the API rejects every sign-in with 409 Device.Blocked . The reason (1 to 500 characters) is mandatory POST /v1/platform/ops/devices/{deviceId}/block · platform.ops.device.reset no ye"
  },
  {
    "id": "cli/commands-platform-operations#incidents-and-maintenance-windows",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Incidents and maintenance windows",
    "anchor": "incidents-and-maintenance-windows",
    "text": "Command Purpose Route and permission Key Modifies incident list The incident reports, with --id=<inc_…> a single one including its history GET /v1/platform/ops/incidents · platform.ops.incident.list , with --id GET …/incidents/{id} · platform.ops.incident.read no no incident create --title=… --severity=sev-1…sev-4 --scope=platform|tenant|device-fleet Open an incident. --public also puts it on the status page, and without the flag it stays internal POST /v1/platform/ops/incidents · platform.ops.incident.open no yes incident update <inc_…> --message=… Add an entry to the history. --status advances the status at the same time, --public puts the incident on the status page, --internal takes it down PATCH /v1/platform/ops/incidents/{id} · platform.ops.incident.update no yes incident resolve <inc_…> Close an incident, plus --postmortem=<url> POST /v1/platform/ops/incidents/{id}/resolve · platform.ops.incident.resolve no yes maintenance-window list The planned maintenance windows, with --id=<mtw_…> a single one GET /v1/platform/ops/maintenance-windows · platform.ops.maintenance.list , with --id platform.ops.maintenance.read no no maintenance-window create --title=… --start=… --end=… --scope=… Plan a maintenance window. --banner , --notify-days=<tage> , and --block-mutations , which halts the organizations’ write calls POST /v1/platform/ops/maintenance-windows · platform.ops.maintenance.create no yes maintenance-window update <mtw_…> Move or rewrite a window PATCH /v1/platform/ops/ma"
  },
  {
    "id": "cli/commands-platform-operations#queues-cache-events-and-connections",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Queues, cache, events, and connections",
    "anchor": "queues-cache-events-and-connections",
    "text": "Command Purpose Route and permission Key Modifies job queues The queues with metrics and state, from three sources in one table GET …/ops/jobs/queues , GET …/ops/jobs/metrics , GET …/ops/jobs/health · platform.jobs.queues.read , platform.jobs.metrics.read , platform.jobs.health.read yes no job pause <name> Pause a queue. What is running finishes POST /v1/platform/ops/jobs/queues/{queueName}/pause · platform.jobs.queues.pause no yes job resume <name> Resume a paused queue POST /v1/platform/ops/jobs/queues/{queueName}/resume · platform.jobs.queues.resume no yes job failed The jobs that failed for good, with --not-replayed , --queue=<name> , --id=<kennung> GET /v1/platform/ops/jobs/dead-letter , GET …/dead-letter/{id} · platform.jobs.dead-letter.read yes no job replay --id=<kennung>|--all --reason=… Enqueue a failed job, or many, again. --max (default 100) caps the number POST …/jobs/dead-letter/{id}/replay , POST …/jobs/dead-letter/bulk-replay · platform.jobs.replay no yes job kill <jobId> --queue=<name> --reason=… --yes Abort a running job, response 202 POST /v1/platform/ops/jobs/{queueName}/{jobId}/force-kill · platform.jobs.force-kill no yes job cleanup --yes Clear out old dead-letter entries POST /v1/platform/ops/jobs/cleanup/run · platform.jobs.cleanup no yes cache stats State, hit rate, and the hot topics, plus --window=1m|5m|1h GET …/ops/cache/health , GET …/ops/cache/stats , GET …/ops/cache/topics · platform.cache.stats.read no no cache invalidate --topic=<thema>|--patt"
  },
  {
    "id": "cli/commands-platform-operations#error-and-permission-catalog",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Error and permission catalog",
    "anchor": "error-and-permission-catalog",
    "text": "Command Purpose Route and permission Key Modifies error catalog All error codes of the API with HTTP status, plus --search=<text> GET /v1/platform/ops/errors/catalog · platform.errors.catalog.read no no permission catalog The permission catalog. --key-eligible shows what an operator key may carry GET /v1/permission-catalog , sign-in only n/a no permission drift What the inventory lacks compared with the catalog. Changes nothing GET /v1/platform/ops/permissions/catalog-drift · platform.permissions.catalog.read no no permission reload Reload the catalog, which is needed when app permissions are added POST /v1/platform/ops/permissions/catalog/reload · platform.permissions.catalog.reload no yes permission backfill Bring the roles of the organizations up to date with the catalog, plus --tenant=<tnt_…> . --dry-run only calculates, --reconcile also removes. Operator accounts do not need this: their permissions follow the role POST …/ops/permissions/roles/backfill · platform.permissions.role.backfill no yes permission flush-cache --yes Discard the cache of effective permissions POST /v1/platform/ops/permissions/cache/flush · platform.permissions.cache.flush no yes"
  },
  {
    "id": "cli/commands-platform-operations#organizations",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Organizations",
    "anchor": "organizations",
    "text": "Command Purpose Route and permission Key Modifies tenant list The organizations of the platform ( active , frozen , suspended , deleted ) GET /v1/platform/tenants · platform.tenant.list no no tenant get <tnt_…> An organization in the operations view GET /v1/platform/tenants/{id} · platform.tenant.read no no tenant provision --slug=… --name=… --owner-email=… --address=… Organization, contract, owner, and invitation in one go, plus --licenses , --set=<k=v> POST /v1/platform/tenants/provision · platform.tenant.provision , with --set PATCH …/tenants/{tenantId}/feature-entitlements · platform.billing.override no yes tenant overview <tnt_…> The operations record: fleet, content, contacts GET /v1/platform/tenants/{tenantId}/overview · platform.tenant.read no no tenant suspend <tnt_…> --reason=… --yes Suspend an organization, which cascades to its accounts POST /v1/platform/tenants/{id}/suspend · platform.tenant.suspend no yes tenant resume <tnt_…> Lift a suspension POST /v1/platform/tenants/{id}/resume · platform.tenant.resume no yes tenant delete <tnt_…> --slug=… --yes Delete an organization. This cannot be undone. The short name is the confirmation. Its devices are released and then show a pairing code DELETE /v1/platform/tenants/{id} · platform.tenant.delete no yes tenant entitlements <tnt_…> Read contractual capabilities and set them with --set=<k=v> (switch on or off ) GET …/tenants/{tenantId}/feature-entitlements · platform.billing.invoice.read , PATCH … · platform.billing.ove"
  },
  {
    "id": "cli/commands-platform-operations#operator-accounts-and-operator-keys",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Operator accounts and operator keys",
    "anchor": "operator-accounts-and-operator-keys",
    "text": "Command Purpose Route and permission Key Modifies operator list The operator accounts, with --role=owner|manager|support|viewer , --status , --search , and --with-permission=<platform.…> (accounts whose role carries the permission) GET /v1/platform/operators · platform.operator.list no no operator get <ops_…> An operator account with its role and the level of the role per bundle GET /v1/platform/operators/{id} · platform.operator.list no no operator create --email=… --name=… --role=manager|support|viewer Create an operator account. The role is required, owner is not available here. The two-factor setup appears exactly once POST /v1/platform/operators · platform.operator.create no yes operator set-role <ops_…> --role=manager|support|viewer --yes Replace the role. Only the owner, never the own role, never the owner’s. The new permissions apply with the next request of the account PATCH /v1/platform/operators/{id}/role · platform.operator.create no yes operator transfer-owner <ops_…> --totp-code=<123456> Transfer the owner role, step 1: the owner makes the account an offer. The offer is valid for seven days, and a new one replaces the earlier one. --accept --totp-code=… (step 2, under the profile of the target) accepts, after which the target is owner and the former owner is manager. --status shows the open offer POST /v1/platform/operators/{id}/owner-transfer , POST …/operators/owner-transfer/accept , GET …/operators/owner-transfer · platform.operator.create , on acceptance and"
  },
  {
    "id": "cli/commands-platform-operations#device-software",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Device software",
    "anchor": "device-software",
    "text": "The component is always the first word after the command: os , agent , renderer , host-webos , or host-tizen . A version is named by the command with the build identifier (twelve hex characters), and list shows it. Command Purpose Route and permission Key Modifies device-software list <component> The versions in storage, newest first: display name with world ( 0.6.1-prod ), build, state, the channels in which it applies, the number of devices, and the size GET /v1/platform/ops/device-software/{component}/builds · platform.ops.device-software.read yes no device-software releases <component> What applies in the channels, and the history of releases, with --limit and --cursor GET …/device-software/{component}/releases · platform.ops.device-software.read yes no device-software devices <component> The devices that run the component, with channel, version, build, and state relative to the release ( current , updating , rolled-back , outdated , no-release ). Plus --channel , --build , --state GET …/device-software/{component}/devices · platform.ops.device-software.read yes no device-software activity The history across all components: releases, imports, deletions, requests, and the rollbacks that devices reported. Plus --component=<component> GET …/device-software/activity · platform.ops.device-software.read yes no device-software release <component> <build> --channel=… --yes Make a version apply in a channel. --pull-along=beta,canary pulls lower channels along in the same step, --n"
  },
  {
    "id": "cli/commands-platform-operations#requests-to-a-second-person",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Requests to a second person",
    "anchor": "requests-to-a-second-person",
    "text": "The same for device software and apps. Whether a release to stable needs a second person is set by the owner with settings set releaseApproval.deviceSoftware|apps stable . Command Purpose Route and permission Key Modifies approval list The requests, without --state the open ones. Plus --state=open|approved|withdrawn|expired , --kind=device-software|app , --package=<component or app> GET /v1/platform/ops/approvals · platform.ops.device-software.read or platform.app-catalog.list yes no approval approve <rap_…> Approve a request as the second person and thereby execute it. The person who made the request gets a one-line message and exit 4 ( ReleaseApproval.SamePerson ) POST /v1/platform/ops/approvals/{id}/approve · platform.ops.release.approve no yes approval withdraw <rap_…> Withdraw a request, or reject it as another person. --reason=… is optional POST /v1/platform/ops/approvals/{id}/withdraw · platform.ops.release.approve or platform.ops.device-software.release or platform.app-catalog.promote no yes"
  },
  {
    "id": "cli/commands-platform-operations#platform-settings",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Platform settings",
    "anchor": "platform-settings",
    "text": "Command Purpose Route and permission Key Modifies settings get All platform settings in full, one that is not set with its default GET /v1/platform/settings · platform.settings.read no no settings set <setting> <value> --yes Change a setting, for example releaseApproval.apps stable or retention.deviceSoftware.keepNewest 3 . The value is checked against the contract before anything goes out PATCH /v1/platform/settings · platform.settings.manage (owner only) no yes The settings are releaseApproval.deviceSoftware and releaseApproval.apps ( off or stable , default off ) as well as retention.deviceSoftware.keepNewest (1 to 100, default 5) and retention.deviceSoftware.auto (default false ). In addition there are retention.apps.keepNewest (1 to 100, default 5), retention.apps.unreferencedMonths (1 to 120, default 12), and retention.apps.auto (default false ). Every change appears in the audit log with the old and the new value ( platform.settings.changed )."
  },
  {
    "id": "cli/commands-platform-operations#app-catalog",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "App catalog",
    "anchor": "app-catalog",
    "text": "Command Purpose Route and permission Key Modifies app-release list The releases of the app catalog, with --app=<app-id> , --channel=canary|beta|stable|deprecated|retired GET /v1/platform/app-catalog/apps , GET …/apps/{appId}/releases · platform.app-catalog.list no no app-release set-notes <arl_…> --app=<app-id> --notes=… Change the release notes (up to 2000 characters) PATCH …/apps/{appId}/releases/{id} · platform.app-catalog.update no yes app-release promote <arl_…> --app=<app-id> --channel=beta|stable Promote to beta or stable, always one step at a time. To stable, the command promotes immediately if the platform does not require a second person ( releaseApproval.apps = off ), otherwise it creates a request, and --reason=… (10 to 500 characters) is then mandatory POST …/apps/{appId}/releases/{id}/promote · platform.app-catalog.promote no yes app-release restore <arl_…> --app=<app-id> --reason=… Roll back an outdated or superseded version to stable. Immediately or, depending on the setting, as a request that a second person confirms with approval approve . The version that applied until then goes to deprecated POST …/apps/{appId}/releases/{id}/restore · platform.app-catalog.promote no yes app-release demote <arl_…> --app=<app-id> --channel=deprecated Demote a release from stable to deprecated, the only step backward POST …/apps/{appId}/releases/{id}/demote · platform.app-catalog.promote no yes app-release retire <arl_…> --app=<app-id> --reason=… --grace=regular|security|imme"
  },
  {
    "id": "cli/commands-platform-operations#billing-and-items",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Billing and items",
    "anchor": "billing-and-items",
    "text": "Command Purpose Route and permission Key Modifies subscription set-payment-way <tnt_…> --way=invoice|stripe|none Set the payment way of a contract PUT /v1/platform/tenants/{tenantId}/billing/payment-way · platform.billing.override no yes subscription approve-invoice <tnt_…> --yes Approve the purchase on invoice. --revoke withdraws the approval PUT /v1/platform/tenants/{tenantId}/billing/payment-way · platform.billing.override no yes subscription check-stripe <tnt_…> Check the link of a contract to the payment provider. Changes nothing GET /v1/platform/tenants/{tenantId}/billing/stripe-link · platform.billing.override no no subscription relink-stripe <tnt_…> --yes Link a contract to the payment provider again. If the customer still exists there, nothing happens ( unchanged ) POST /v1/platform/tenants/{tenantId}/billing/relink-stripe · platform.billing.override no yes subscription backfill-period-prices --yes Catch up the per-period unit prices of the existing contracts: the fix when the run at application startup failed. Idempotent POST /v1/platform/billing/period-unit-prices/backfill · platform.billing.plan.manage no yes invoice list --tenant=<tnt_…>|--all-tenants The invoices of all organizations GET /v1/platform/invoices · platform.billing.invoice.read no no invoice get <inv_…> An invoice with its line items GET /v1/platform/invoices/{id} · platform.billing.invoice.read no no invoice mark-paid <inv_…> --yes Book a payment received POST /v1/platform/invoices/{id}/mark-paid ·"
  },
  {
    "id": "cli/commands-platform-operations#related",
    "path": "/cli/commands-platform-operations",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "commands-platform-operations",
    "pageTitle": "Commands for platform operations",
    "section": "Related",
    "anchor": "related",
    "text": "Commands for your organization Profiles and credentials"
  },
  {
    "id": "cli/output-and-exit-codes",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "JSON output and exit codes",
    "anchor": "",
    "text": "What the CLI writes to which output and which exit code means which kind of error: the table a script branches on, without comparing any text."
  },
  {
    "id": "cli/output-and-exit-codes#the-output-format",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "The output format",
    "anchor": "the-output-format",
    "text": "Situation What the CLI does Standard output is not a terminal (pipe, file) JSON, without --json being needed Standard output is a terminal Header line and table for humans --json or --table forces the format and takes precedence over the environment variable SZENERA_OUTPUT=json or table forces the format and takes precedence over the detection In JSON mode, standard output carries the payload and nothing else : no envelope, no ok , no dauerMs . A list is therefore .items[] , not .daten.items[] , and nextCursor appears directly in the response as long as more pages follow: For humans the opposite holds: the header line always comes first, even for a write command without a table. Below it comes the table or, if the command has none, the data as JSON."
  },
  {
    "id": "cli/output-and-exit-codes#errors-go-to-standard-error",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "Errors go to standard error",
    "anchor": "errors-go-to-standard-error",
    "text": "Errors always land on standard error, never on standard output. If szenera device list > geraete.json fails, it leaves no file that looks like a result. In JSON mode the error carries its kind as a word, so a log stays readable later without this table: code , status , traceId , details , and retryAfterSeconds are present when the error came from the API. In table mode the same message reads ✗ … , followed by HTTP 403 · Permission.Denied and the trace line Spur trc_… . Progress lines of waiting commands also go to standard error, and standard output carries exactly one document at the end."
  },
  {
    "id": "cli/output-and-exit-codes#the-exit-codes",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "The exit codes",
    "anchor": "the-exit-codes",
    "text": "Code Kind Where it comes from 0 ok all good 1 usage unknown command, missing positional argument, unknown flag, flag without a value, missing --yes , wrong ID prefix, and a request that contradicts the contract (with field path) 2 config profile missing or broken, no key prefix, profile blocked ( readOnly: true ) 3 auth HTTP 401 4 forbidden HTTP 403 5 not-found HTTP 404 6 conflict HTTP 409, a state or version conflict 7 validation HTTP 422, the server rejected the input 8 rate-limited HTTP 429. retryAfterSeconds is in the error 9 server HTTP 5xx or a response that contradicts the contract 10 network no HTTP response at all: no network, no name resolution, aborted 11 failed the operation went through and ended in failure (processing failed, upload rejected, command failed ) 12 timeout the time limit was reached. The outcome is unknown Codes 1 and 2 are errors on your side, 3 to 8 are refusals by the server with a clear cause, 9 and 10 are causes where a later attempt makes sense, and 11 and 12 are the outcomes of an operation that ends long after its call. Other status codes below 500 that match no row end as 1."
  },
  {
    "id": "cli/output-and-exit-codes#11-and-12-are-not-9",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "11 and 12 are not 9",
    "anchor": "11-and-12-are-not-9",
    "text": "Both went through: the call was right, and the server answered. With 11 the result is negative. asset upload was able to push the bytes, but processing failed, and the finding is in the error. With 12 the result is open, because the operation may still be running. Anyone who treats 12 like “failed” triggers a running operation a second time. Anyone who treats 11 like 9 repeats a call whose answer is already settled."
  },
  {
    "id": "cli/output-and-exit-codes#request-and-response-are-two-sides",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "Request and response are two sides",
    "anchor": "request-and-response-are-two-sides",
    "text": "The generated client checks both directions against the same schemas. If no network packet went out for the call, the request was at fault: exit 1, with a field path: If the response breaks the contract, the usual reason is a version skew: the other end is older or newer than the CLI and does not send a field that the CLI already requires. The message says so, names the places, and shows the way to the raw response: --raw applies everywhere and, for exactly this error, writes the raw response to standard output ( null if no body arrived). The diagnosis stays on standard error, and the exit code stays 9. It is the only place where a failed call writes anything to standard output, so the document can go through jq instead of being fished out of an error message."
  },
  {
    "id": "cli/output-and-exit-codes#related",
    "path": "/cli/output-and-exit-codes",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "output-and-exit-codes",
    "pageTitle": "JSON output and exit codes",
    "section": "Related",
    "anchor": "related",
    "text": "Scripts and CI When the CLI aborts The error format"
  },
  {
    "id": "cli/scripts-and-ci",
    "path": "/cli/scripts-and-ci",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "scripts-and-ci",
    "pageTitle": "Scripts and CI",
    "section": "Scripts and CI",
    "anchor": "",
    "text": "After this guide, the CLI runs in a pipeline without a profile file. Your script reacts to exit codes instead of text, fetches lists completely, limits wait times, and retries calls that change something without triggering an operation twice."
  },
  {
    "id": "cli/scripts-and-ci#steps",
    "path": "/cli/scripts-and-ci",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "scripts-and-ci",
    "pageTitle": "Scripts and CI",
    "section": "Steps",
    "anchor": "steps",
    "text": "Store the address and key as secrets of your CI and pass them into the run as SZENERA_BASE_URL and SZENERA_API_KEY . Together, the two variables carry a complete profile. No file on disk is needed, and the profile is not write-protected. Call the CLI directly through the built file, never through pnpm run , or your script sees the code 1 for every error: At the start of every pipeline, check the environment and the credential with two calls that separate network from key: Evaluate the exit code, not the message. On failure, standard output carries nothing, so it can safely go into a file: Process the JSON output with jq . Lists carry items and, as long as pages follow, nextCursor :"
  },
  {
    "id": "cli/scripts-and-ci#options",
    "path": "/cli/scripts-and-ci",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "scripts-and-ci",
    "pageTitle": "Scripts and CI",
    "section": "Options",
    "anchor": "options",
    "text": "Fetch lists completely. --all follows all pages until there are no more and returns one concatenated list without nextCursor . For large collections, combine it with --limit=200 , the cap per page: Limit wait times. Commands that wait for an operation ( asset upload , asset reprocess , device screenshot , device send-command --wait ) accept --timeout=<dauer> ( 90s , 20m , 2h , or a number in seconds) and some also --interval=<dauer> . --no-wait only triggers the operation and returns immediately, without knowing the outcome. Exit 12 always means that the outcome is unknown and the operation may still be running. Make retries safe. For the escape hatch api post|put|patch|delete , the CLI generates a fresh idempotency key for every call. --idempotency-key=<schluessel> is needed only to continue an aborted retry, and then with the same value as on the first attempt. Commands that are idempotent by themselves say so in their help: sending player set-program twice is not a second operation. Protection against irreversible actions. Commands that do something irreversible require --yes and abort with exit 1 without the flag: asset purge , asset delete , scene delete , api-key revoke , and more. szenera <befehl> --help names the flag. For profiles in a file, readOnly: true remains the lock: a command that changes something then aborts with exit 2 before a network packet is created. A profile made only of environment variables does not have this lock."
  },
  {
    "id": "cli/scripts-and-ci#check-the-result",
    "path": "/cli/scripts-and-ci",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "scripts-and-ci",
    "pageTitle": "Scripts and CI",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "szenera health and szenera whoami run at the start of the pipeline with exit 0. whoami names the expected organization under tenantId . A deliberately wrong call ( szenera device list --state=active ) ends with exit 1 and a message on standard error. The redirected file stays empty. After an upload, szenera asset get <ast_…> --json shows the state ready ."
  },
  {
    "id": "cli/scripts-and-ci#if-something-goes-wrong",
    "path": "/cli/scripts-and-ci",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "scripts-and-ci",
    "pageTitle": "Scripts and CI",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution Every error ends with 1 , even a 401 The call runs through pnpm run . Call the file directly through node (step 2). Keine Konfiguration gefunden (no configuration found) (exit 2) SZENERA_BASE_URL is missing or empty. Set both variables in the run. See Profiles and credentials . Exit 3 on every command except health The key is not valid: revoked, expired, or issued for another environment. Create a new key and replace the secret. Exit 4 on a single command The key does not carry the permission, or the permission is not meant for keys. Check the “Key” column under Commands for your organization . Otherwise use a profile with password sign-in. Exit 12 after an upload The time limit was too tight. Processing continues. Set --timeout=20m and check with asset get before a retry."
  },
  {
    "id": "cli/scripts-and-ci#related",
    "path": "/cli/scripts-and-ci",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "scripts-and-ci",
    "pageTitle": "Scripts and CI",
    "section": "Related",
    "anchor": "related",
    "text": "JSON output and exit codes Profiles and credentials Idempotency from the start"
  },
  {
    "id": "cli/troubleshooting",
    "path": "/cli/troubleshooting",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "troubleshooting",
    "pageTitle": "When the CLI aborts",
    "section": "When the CLI aborts",
    "anchor": "",
    "text": "The most common aborts of the CLI, in the order in which they occur: with the message you see, the exit code, its meaning, and the way back. Each solution points to the page that covers the topic. Symptom Meaning Solution Unbekannter Befehl „device lister\". (unknown command) and below it the whole overview (exit 1) The words match no command in the registry: a typo, the wrong order ( list device ), or a flag without = that was read as a word. Choose the command from the overview. Flags carry their value after the equals sign ( --limit=20 ). See Commands for your organization . „device list\" kennt die Flagge „--state\" nicht. Bekannt sind: … (unknown flag) (exit 1) An unknown flag is an error, not a silent hint. The message lists the valid ones. Use one of the flags the message names. szenera <befehl> --help explains them. „--limit\" braucht einen Wert: --limit=<anzahl>. (flag needs a value) (exit 1) The flag was given without an equals sign. --limit 20 does not exist. Write --limit=20 . „device get\" braucht <id> — … (positional argument missing) (exit 1) A required positional argument is missing. Append the ID as --help shows it. „scn_01K…\" ist keine Kennung für ein Medium: erwartet wird ast_… (not an ID for a media item) (exit 1) The prefix of the ID does not match the command. No call went out. Use the right ID: ast_ media item, scn_ scene, pll_ playlist, sch_ program, ply_ display, dev_ device. … wandert in den Papierkorb. Wenn das gewollt ist: --yes anhängen. (moves to the "
  },
  {
    "id": "cli/troubleshooting#related",
    "path": "/cli/troubleshooting",
    "area": "cli",
    "areaTitle": "CLI",
    "page": "troubleshooting",
    "pageTitle": "When the CLI aborts",
    "section": "Related",
    "anchor": "related",
    "text": "Profiles and credentials JSON output and exit codes Scripts and CI"
  },
  {
    "id": "webhooks/index",
    "path": "/webhooks",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "index",
    "pageTitle": "Webhooks",
    "section": "Webhooks",
    "anchor": "",
    "text": "Events from your organization to your systems: create an endpoint, verify the signature, understand retries, look up event types. The Settings › Organization › Webhooks page with the Webhook endpoints list, the created endpoints in Active status, and the expandable signature verification guide below All guides in this area work on this page: Settings › Organization › Webhooks."
  },
  {
    "id": "webhooks/index#understand",
    "path": "/webhooks",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "index",
    "pageTitle": "Webhooks",
    "section": "Understand",
    "anchor": "understand",
    "text": "Page When you need it How is a webhook delivered? From the event through matching against your endpoints, signing, and sending, to a retry or disabling. What is sent and how it is retried The request to your endpoint (method, headers, body), the states of a delivery, the retry schedule, and disabling."
  },
  {
    "id": "webhooks/index#set-up",
    "path": "/webhooks",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "index",
    "pageTitle": "Webhooks",
    "section": "Set up",
    "anchor": "set-up",
    "text": "Page When you need it Create a webhook endpoint Enter an https address and the subscribed events under Settings › Organization › Webhooks, and store the signing secret safely. Verify the signature of a delivery Recompute HMAC-SHA256 over the raw body, compare it with X-Signature in constant time, deduplicate by X-Delivery-Id, and rotate the secret."
  },
  {
    "id": "webhooks/index#events",
    "path": "/webhooks",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "index",
    "pageTitle": "Webhooks",
    "section": "Events",
    "anchor": "events",
    "text": "Page When you need it Event types: organization, accounts, billing The subscribable event types from organization, storage connections, access, accounts and delegated access, sessions, billing, and joining, with their core payload. Event types: content, programs, devices, apps The subscribable event types from media, scenes, programs, marketplace, fleet, app runtime, playback, analytics, and advertising, plus the platform types that never arrive. Fetch the event catalog through the API You create endpoints from your own system or show the selection in your own interface: fetch the catalog as JSON and check a list of events before the server rejects it."
  },
  {
    "id": "webhooks/index#if-something-goes-wrong",
    "path": "/webhooks",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "index",
    "pageTitle": "Webhooks",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Page When you need it When no delivery arrives Endpoint rejected, inactive after failures, no retry after a 4xx, signature does not match, duplicate deliveries, an event never occurs."
  },
  {
    "id": "webhooks/about-delivery",
    "path": "/webhooks/about-delivery",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "about-delivery",
    "pageTitle": "How is a webhook delivered?",
    "section": "How is a webhook delivered?",
    "anchor": "",
    "text": "A webhook is a request that the platform sends to an address of your choice as soon as something happens in your organization. Here is the path from the event to your server’s response, and what happens when it does not respond."
  },
  {
    "id": "webhooks/about-delivery#from-event-to-delivery",
    "path": "/webhooks/about-delivery",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "about-delivery",
    "pageTitle": "How is a webhook delivered?",
    "section": "From event to delivery",
    "anchor": "from-event-to-delivery",
    "text": "Every business event that is raised in the context of your organization is matched against your webhook endpoints. It reaches every endpoint that is active and has subscribed to this event type. The match knows three forms and no others: the exact event type, the whole area ( composition.* ), and * for everything. composition.scene.published therefore reaches an endpoint whose list contains this text, composition.* , or * . For every matching endpoint, a separate delivery is created with its own ID ( whd_… ). Its body is the payload of the event as JSON. The platform signs this body with the secret of the endpoint (HMAC-SHA256) and sends it by POST to your address, together with the headers X-Signature , X-Event-Type , and X-Delivery-Id . Your server has ten seconds. If it responds with a 2xx status, the delivery counts as delivered."
  },
  {
    "id": "webhooks/about-delivery#if-the-receiver-does-not-respond",
    "path": "/webhooks/about-delivery",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "about-delivery",
    "pageTitle": "How is a webhook delivered?",
    "section": "If the receiver does not respond",
    "anchor": "if-the-receiver-does-not-respond",
    "text": "If your server responds with 5xx, 408, or 429, or does not respond at all (network error, timeout), the platform treats this as a temporary disruption and retries: at most five attempts, with waits of 2, 4, 8, and 16 minutes, which a job running every five minutes picks up. After the fifth failure the delivery has failed for good. The exact values are in the retry schedule . Any other 4xx response (such as 400, 401, or 404) counts as an error on your side that a retry does not fix. The delivery fails for good immediately, without another attempt. After that, it can be triggered again only through the API."
  },
  {
    "id": "webhooks/about-delivery#if-an-endpoint-keeps-failing",
    "path": "/webhooks/about-delivery",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "about-delivery",
    "pageTitle": "How is a webhook delivered?",
    "section": "If an endpoint keeps failing",
    "anchor": "if-an-endpoint-keeps-failing",
    "text": "Every failure raises a counter on the endpoint, and every success resets it to zero. More than 20 failures in a row disable the endpoint: it becomes inactive, receives no new deliveries, and the platform records the event notifications.webhook.endpoint.disabled . An hourly job also checks whether an active endpoint is over the threshold. After you fix the cause, turn the endpoint back on in the portal with Activate . This takes effect up to an hour later. Two kinds of events never reach an endpoint: the events of delivery itself ( notifications.* ), because otherwise every delivery would trigger the next one, and events without a link to an organization, such as those from platform operations. Flow of a delivery: event, match against the active endpoints, signing, POST with a ten-second timeout, then Delivered on 2xx or retry on 5xx, 408, 429, and network errors. Other 4xx responses fail immediately. More than 20 failures in a row disable the endpoint"
  },
  {
    "id": "webhooks/about-delivery#what-follows",
    "path": "/webhooks/about-delivery",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "about-delivery",
    "pageTitle": "How is a webhook delivered?",
    "section": "What follows",
    "anchor": "what-follows",
    "text": "Respond with 2xx right away and process afterward. The ten-second timeout applies to the whole request. If you process first and respond afterward, you risk unnecessary retries. Deduplicate by X-Delivery-Id. A retry carries the same ID as the first attempt. If you store the ID, you process each delivery exactly once, even if your first response got lost on the way. Respond with 4xx only if the delivery is truly unusable. A 4xx ends the delivery for good. If your system is only temporarily unavailable, respond with 503 or 429, and the platform will try again. Check the delivery log when something is missing. For each delivery it shows the state, the response code, and the number of attempts."
  },
  {
    "id": "webhooks/about-delivery#related",
    "path": "/webhooks/about-delivery",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "about-delivery",
    "pageTitle": "How is a webhook delivered?",
    "section": "Related",
    "anchor": "related",
    "text": "What is sent and how it is retried Verify the signature of a delivery Event types: organization, accounts, billing"
  },
  {
    "id": "webhooks/delivery-and-retries",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "What is sent and how it is retried",
    "anchor": "",
    "text": "The request your endpoint receives, the states of a delivery, the retry schedule, and disabling, for reference when you build and operate a receiver."
  },
  {
    "id": "webhooks/delivery-and-retries#the-request-to-your-endpoint",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "The request to your endpoint",
    "anchor": "the-request-to-your-endpoint",
    "text": "Every delivery is a POST to the address of the endpoint. The body is the payload of the event as JSON, serialized compactly (without spaces and line breaks). Only the payload travels. The time of the event, its identifier, and the trigger are not in the body. Component Value Method POST Content-Type application/json X-Signature HMAC-SHA256 over the raw body, with the signing secret of the endpoint, base64url encoded X-Event-Type The event type, for example composition.scene.published X-Delivery-Id The ID of the delivery ( whd_ and 26 characters). A retry carries the same one Body The payload of the event as JSON, the fields from the event tables Timeout 10 seconds for connection and response This is what a delivery of the event composition.scene.published looks like (body wrapped for readability, in reality it arrives on one line):"
  },
  {
    "id": "webhooks/delivery-and-retries#the-expected-response",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "The expected response",
    "anchor": "the-expected-response",
    "text": "Your response Effect Status 2xx within 10 seconds The delivery counts as delivered. The failure counter of the endpoint resets to zero 5xx, 408, or 429 Temporary failure: retry according to the schedule below No response within 10 seconds, connection error Like a temporary failure: retry Any other 4xx Permanent failure: no retry, the delivery has failed for good On a failure, the response text is stored truncated (at most 4 kB) in the delivery log. On a successful delivery, only the status code is recorded. A short, descriptive text helps when you check later, but it is not required."
  },
  {
    "id": "webhooks/delivery-and-retries#delivery-states",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "Delivery states",
    "anchor": "delivery-states",
    "text": "State in the API In the portal Meaning pending Pending Sending is in progress or imminent delivered Delivered Your server responded with 2xx. deliveredAt is set failed Failed Temporary failure. nextRetryAt gives the next attempt dead dead Failed for good: fifth failure or a 4xx response. deadAt is set The path is pending → delivered or failed . From failed , every further attempt leads back through pending to delivered , failed , or dead . For the state dead , the portal has no label of its own, and the delivery log shows the value itself. The label Retrying exists in the portal but is not assigned to any state."
  },
  {
    "id": "webhooks/delivery-and-retries#retry-schedule",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "Retry schedule",
    "anchor": "retry-schedule",
    "text": "The platform retries only on 5xx, 408, 429, timeouts, and network errors. There are at most five attempts per delivery. The wait after the nth failure is 1 minute · 2ⁿ, with ±20% jitter. A job that runs every five minutes picks up due retries, up to 100 per run, so the actual wait is up to five minutes longer than the target value. Attempt Wait after failure On another failure 1 2 minutes ±20% Attempt 2 2 4 minutes ±20% Attempt 3 3 8 minutes ±20% Attempt 4 4 16 minutes ±20% Attempt 5 5 — Failed for good ( dead ) A 4xx response other than 408 and 429 ends the delivery immediately, regardless of the attempt counter."
  },
  {
    "id": "webhooks/delivery-and-retries#redeliver-by-hand",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "Redeliver by hand",
    "anchor": "redeliver-by-hand",
    "text": "In the delivery log of the endpoint, every delivery with the status Failed has the Retry button. For a delivery in the state dead , the portal shows no button. You trigger a redelivery through the API: The response (status 201) is the delivery with the outcome of the new attempt. The new attempt is signed with the currently valid secret of the endpoint and carries the same X-Delivery-Id . If the endpoint is inactive, nothing is sent, and the response carries the delivery unchanged. The API does not check the state: even a delivery that was already delivered can be sent again this way. Your receiver then sees the same ID a second time."
  },
  {
    "id": "webhooks/delivery-and-retries#disabling-an-endpoint",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "Disabling an endpoint",
    "anchor": "disabling-an-endpoint",
    "text": "Every failure raises the consecutiveFailures counter of the endpoint, and every success resets it to zero. If the counter is above 20, the endpoint becomes inactive ( active: false ), and the platform records notifications.webhook.endpoint.disabled . An hourly job makes up for what the counter missed at sending time. An inactive endpoint receives no new deliveries. In the portal it shows Inactive with the note “last failure …”. To turn it back on, use Activate or PATCH /v1/webhooks/{id} with {\"active\": true} . This does not take effect immediately: for each event type, the platform remembers for up to an hour whether an active endpoint has subscribed to it. After activating, or after adding events, it can therefore take up to an hour until the first delivery of a newly subscribed type arrives. Newly created endpoints take effect immediately."
  },
  {
    "id": "webhooks/delivery-and-retries#the-delivery-log-through-the-api",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "The delivery log through the API",
    "anchor": "the-delivery-log-through-the-api",
    "text": "The response is a page with items and nextCursor (if there is more). The next call gets cursor=<nextCursor> . limit is between 1 and 200, and 50 if omitted. Field Meaning id ID of the delivery, the same as in X-Delivery-Id endpointId , tenantId Endpoint and organization eventType The event type payloadSignature The signature that was sent as X-Signature status pending , delivered , failed , or dead attemptCount Number of attempts so far responseCode , responseBody Response from your server. responseCode is null if there was none. responseBody is filled only for failed attempts (truncated to 4 kB) and after a successful delivery is null or still carries the text of the last failed attempt nextRetryAt Next scheduled attempt, only for failed dispatchedAt , deliveredAt , deadAt , createdAt Times of the last send, the delivery, the final failure, and creation The body that was sent is not in the list."
  },
  {
    "id": "webhooks/delivery-and-retries#permissions",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "Permissions",
    "anchor": "permissions",
    "text": "Action Permission Roles Read the delivery log webhook.delivery.list Owner, Manager Redeliver webhook.delivery.retry Owner These permissions apply to signed-in accounts. An API key ( ak_… ) cannot carry them. The calls need the access token of a sign-in, see Authentication ."
  },
  {
    "id": "webhooks/delivery-and-retries#related",
    "path": "/webhooks/delivery-and-retries",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "delivery-and-retries",
    "pageTitle": "What is sent and how it is retried",
    "section": "Related",
    "anchor": "related",
    "text": "How is a webhook delivered? Verify the signature of a delivery When no delivery arrives"
  },
  {
    "id": "webhooks/create-endpoint",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Create a webhook endpoint",
    "anchor": "",
    "text": "By the end, your organization has an active endpoint that receives the chosen events at your https address, and you have stored the signing secret safely."
  },
  {
    "id": "webhooks/create-endpoint#steps",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Steps",
    "anchor": "steps",
    "text": ""
  },
  {
    "id": "webhooks/create-endpoint#in-the-portal",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "In the portal",
    "anchor": "in-the-portal",
    "text": "Open Settings › Organization › Webhooks . Click Create endpoint . The Create webhook endpoint dialog opens. Under Endpoint URL , enter the address of your receiver, for example https://example.com/hooks/signage . Under Subscribed events , choose the event types in the Search event … field. The field filters the catalog, and each choice appears as a chip above the field. At least one event is required. Click Create . The Signing secret — visible only now box appears. Click Copy secret and store the value where your receiver reads it. Only then click Got it . The Create webhook endpoint dialog with the Endpoint URL field (1), the Subscribed events selection with the Search event … search field (2), and the Create button (3) Create becomes available only when an address is entered and at least one event is in the selection as a chip."
  },
  {
    "id": "webhooks/create-endpoint#through-the-api",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Through the API",
    "anchor": "through-the-api",
    "text": "$TOKEN is the access token from signing in as Owner. An API key cannot carry the webhook permissions (see Authentication ). $SZENERA_API is the base URL of your environment. The response (status 201) contains the endpoint and, only in this response, the secret: The secret cannot be retrieved afterward. If it gets lost, renew it (see Options). The server checks the events list before it creates the endpoint. Allowed values are an exact event type, an area such as composition.* , or * for everything. An unknown name is 422 WebhookEndpoint.EventUnknown and is named in the response. A typo should stand out here and not end up as an endpoint that stays silent. GET /v1/webhooks/event-types returns the names that exist, see Fetch the event catalog through the API ."
  },
  {
    "id": "webhooks/create-endpoint#options",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Options",
    "anchor": "options",
    "text": "Task In the portal Through the API Change the address or events Edit → Edit webhook dialog → Save PATCH /v1/webhooks/{id} with url and/or events Pause or resume an endpoint Deactivate / Activate PATCH /v1/webhooks/{id} with {\"active\": false} or {\"active\": true} Renew the secret Rotate secret → new Signing secret — visible only now box POST /v1/webhooks/{id}/rotate-secret → response {\"secret\": \"whsec_…\"} Remove an endpoint Delete → Delete webhook endpoint? dialog → Delete DELETE /v1/webhooks/{id} → {\"deleted\": true} List endpoints List under Webhook endpoints GET /v1/webhooks?limit=50 (page with cursor ) When you edit an endpoint, the signing secret stays unchanged. A deactivated endpoint keeps its address, events, and delivery history. Added events and a re-enabled endpoint take effect up to an hour later. Newly created endpoints take effect immediately."
  },
  {
    "id": "webhooks/create-endpoint#rules-for-the-address-and-events",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Rules for the address and events",
    "anchor": "rules-for-the-address-and-events",
    "text": "https only, public only. The address begins with https:// . The server rejects localhost , ::1 , names ending in .local , and the ranges 127. , 10. , 192.168. , 169.254. , and 172.16. through 172.31. . The response is status 422, WebhookEndpoint.UrlInvalid , both when creating and when changing. At least one event, known names. Allowed are an exact event type, an area such as composition.* , and * . A typo in the type is rejected with 422 WebhookEndpoint.EventUnknown , and so is a type that cannot be subscribed to. The package counts when you create an endpoint and when you renew the secret. Without webhooks in the package, creating an endpoint returns status 403, Billing.FeatureNotEntitled . In the portal, Create endpoint and Rotate secret are locked, while POST /v1/webhooks/{id}/rotate-secret remains possible through the API. Existing endpoints keep running. Permissions. Creating, changing, renewing, and deleting require the Owner role ( webhook.create , webhook.update , webhook.delete ). Owners and Managers see the list ( webhook.list )."
  },
  {
    "id": "webhooks/create-endpoint#check-the-result",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "The endpoint appears in the Webhook endpoints list with the status Active . After the next matching event, the Delivery log shows a row with the status Delivered and the response code of your server (for example “HTTP 200”). Your receiver has seen a request with the headers X-Signature , X-Event-Type , and X-Delivery-Id ."
  },
  {
    "id": "webhooks/create-endpoint#if-something-goes-wrong",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution Status 422, WebhookEndpoint.UrlInvalid The address is not https or points to a private network Use a public https address Status 403, Billing.FeatureNotEntitled . In the portal, Create endpoint and Rotate secret are locked Your package does not include webhooks Change the package. Existing endpoints can still be edited, switched off, and deleted Status 403, Permission.Denied Your account does not have the Owner role, or you are calling with an API key Sign in as an Owner. Use the access token of a sign-in instead of an API key Status 422, WebhookEndpoint.EventUnknown An entry in events matches none of the three allowed forms. details.unknownEvents names it Check the name against the event catalog The endpoint is active, but no delivery ever arrives The event does not occur, or the subscribed events are not yet in effect See When no delivery arrives"
  },
  {
    "id": "webhooks/create-endpoint#related",
    "path": "/webhooks/create-endpoint",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "create-endpoint",
    "pageTitle": "Create a webhook endpoint",
    "section": "Related",
    "anchor": "related",
    "text": "Verify the signature of a delivery Event types: organization, accounts, billing Fetch the event catalog through the API When no delivery arrives"
  },
  {
    "id": "webhooks/verify-signature",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "Verify the signature of a delivery",
    "anchor": "",
    "text": "By the end, your receiver accepts only deliveries that really come from the platform, processes each one exactly once, and survives a change of the secret."
  },
  {
    "id": "webhooks/verify-signature#steps",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "Steps",
    "anchor": "steps",
    "text": "Read the body as raw bytes before anything interprets it as JSON. The signature covers exactly the bytes that were sent. If you parse the body first and serialize it again, you may change the order or spacing and get a different signature. Compute HMAC-SHA256 over these bytes with the secret as the key, and encode the result as base64url, the way Node.js produces it with digest('base64url') . Compare with X-Signature in constant time and only for equal length. An ordinary character comparison stops at the first difference and reveals through its running time how many characters match. Branch by X-Event-Type. The header gives the event type without you having to read the body. Deduplicate by X-Delivery-Id. Remember every processed ID. A retry carries the same ID. If it is known, respond with 2xx and do nothing else. Only now process, and respond with 2xx quickly. The timeout is ten seconds. Do long-running work after you respond. This is what the check looks like in Node.js. It is the example that the portal also shows under Verify deliveries are genuine : Call it from your receiver like this: isFromSignage(rawBody, req.headers['x-signature'], process.env.WEBHOOK_SECRET) . Depending on the server, header names arrive in lowercase. The platform sends X-Signature ."
  },
  {
    "id": "webhooks/verify-signature#options",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "Options",
    "anchor": "options",
    "text": ""
  },
  {
    "id": "webhooks/verify-signature#renew-the-secret",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "Renew the secret",
    "anchor": "renew-the-secret",
    "text": "The secret has the form whsec_ plus at least 32 characters from A–Z , a–z , 0–9 , _ , and - . It is shown only on creation and after a rotation and cannot be retrieved. If it is lost or you want to change it: Set up your receiver so that it can pick up the new secret right after it is shown, for example through a configuration that reloads without a restart. In the portal, click Rotate secret on the endpoint (locked without a package that includes webhooks), or call POST /v1/webhooks/{id}/rotate-secret . Through the API, this remains possible even without a package. Copy the new secret from the Signing secret — visible only now box ( Copy secret ) or from the response {\"secret\": \"whsec_…\"} . From the next send onward, only the new secret is used for signing, including retries of older deliveries. There is no transition phase in which both apply. Until your receiver knows the new secret, it rejects genuine deliveries. The platform retries them only if you respond with 5xx instead of 4xx during that time. The address, events, and delivery history of the endpoint are kept during the rotation."
  },
  {
    "id": "webhooks/verify-signature#check-the-result",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "A genuine delivery passes the check, and in the Delivery log it appears as Delivered with your response code. A request with a modified body or without a valid X-Signature is rejected. A redelivered ID ( Retry in the delivery log) does not trigger a second processing on your side."
  },
  {
    "id": "webhooks/verify-signature#if-something-goes-wrong",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution Signature never matches The body was changed before the check (serialized again, character set converted, whitespace removed) Check the raw bytes before a JSON parser or a framework touches the body Signature no longer matches since a certain point in time The secret was rotated, and your receiver still checks with the old one Switch to the new secret. For deliveries from the gap, use Retry in the delivery log Signature matches, comparison fails anyway Length check or encoding is wrong (hex instead of base64url, padding characters appended) Use exactly the form from the example The same delivery is processed twice Deduplication is missing. A retry or a manual redelivery carried the same X-Delivery-Id Store IDs and acknowledge known IDs with 2xx"
  },
  {
    "id": "webhooks/verify-signature#related",
    "path": "/webhooks/verify-signature",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "verify-signature",
    "pageTitle": "Verify the signature of a delivery",
    "section": "Related",
    "anchor": "related",
    "text": "What is sent and how it is retried Create a webhook endpoint When no delivery arrives"
  },
  {
    "id": "webhooks/event-types-organization",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Event types: organization, accounts, billing",
    "anchor": "",
    "text": "The subscribable event types from organization, storage connections, access, accounts, sessions, billing, and joining, for reference when you create an endpoint or interpret a delivery."
  },
  {
    "id": "webhooks/event-types-organization#how-to-read-the-tables",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Event type is the string you subscribe to on the endpoint. It arrives in the header X-Event-Type with every delivery. Instead of single types you can also subscribe to a whole area ( composition.* ) or to everything with * . There are no other forms, and the server rejects anything else, see Fetch the event catalog through the API . Core payload names two to four of the most important fields that are in the body of the delivery. It is an excerpt. Each area determines the full shape, which can carry more fields. The time, the event identifier, and the trigger do not travel in the body. When it is raised is a short phrase for context, not a statement about frequency. Under Subscribed events , the portal offers exactly the types of this page and of the page Event types: content, programs, devices, apps for selection, the same list that GET /v1/webhooks/event-types returns. Everything else cannot be subscribed to, and creating an endpoint rejects it. Which types these are is described under What never arrives . Only what is raised in the context of your organization is delivered."
  },
  {
    "id": "webhooks/event-types-organization#the-event-types",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "The event types",
    "anchor": "the-event-types",
    "text": ""
  },
  {
    "id": "webhooks/event-types-organization#organization",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Organization",
    "anchor": "organization",
    "text": "Lifecycle of the organization, brand, custom portal address, and company sign-in. tenancy.tenant.suspended and .deleted are the triggers that other areas follow. Event type Core payload When it is raised tenancy.tenant.created tenantId , slug , displayName An organization was created. tenancy.tenant.updated tenantId , changedFields The master data of the organization was changed. tenancy.tenant.suspended tenantId , reason , suspendedById , deletionDueAt The organization was suspended, and deletionDueAt gives the deletion date. tenancy.tenant.reactivated tenantId , reactivatedById A suspension was lifted. tenancy.tenant.frozen tenantId , reason , frozenById The organization was frozen. tenancy.tenant.unfrozen tenantId , unfrozenById The freeze was lifted. tenancy.tenant.deletion-warned tenantId , deletesAt , daysLeft Warning before deletion. daysLeft gives the remaining days. tenancy.tenant.deleted tenantId , slug , deletedById The organization was deleted. tenancy.branding.updated tenantId , changedFields The brand (logo, colors) was changed. tenancy.custom-domain.verified tenantId , customDomain A custom portal address was verified. tenancy.custom-domain.removed tenantId , domain A custom portal address was removed. tenancy.sso.configured tenantId , providerId , protocol , status The company sign-in was set up or changed. tenancy.sso.disabled tenantId , providerId , protocol The company sign-in was switched off. tenancy.sso.domain-changed tenantId , providerId , domain , sta"
  },
  {
    "id": "webhooks/event-types-organization#storage-connections",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Storage connections",
    "anchor": "storage-connections",
    "text": "Standing authorizations for third-party storage accounts. broken tells you since when no new files have arrived. Event type Core payload When it is raised cloud-connections.connection.connected connectionId , provider , accountLabel , repaired A storage account was connected, and repaired distinguishes the first connection from a repair. cloud-connections.connection.disconnected connectionId , provider , revocationAcknowledged The connection was disconnected. cloud-connections.connection.broken connectionId , provider , reason The authorization no longer holds. From here on, syncing stops. It cannot be triggered from the portal."
  },
  {
    "id": "webhooks/event-types-organization#access-roles-and-grants",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Access: roles and grants",
    "anchor": "access-roles-and-grants",
    "text": "Roles of the organization and grants on individual resources. Event type Core payload When it is raised access.role.created roleId , tenantId , name , permissions A role was created. access.role.updated roleId , changedFields , newPermissions , newVersion The permissions or the name of a role were changed. access.role.deleted roleId , tenantId , requesterId A role was deleted. access.grant.created grantId , userId , resourceType , resourceId A grant on a single resource was issued. access.grant.revoked grantId , userId , resourceType , resourceId A grant was revoked. access.grant.expired grantId , userId , resourceId , expiredAt A time-limited grant has expired. access.permission-catalog.rebuilt tenantId , trigger , entryCount The permission catalog of the organization was rebuilt, and trigger gives the reason."
  },
  {
    "id": "webhooks/event-types-organization#accounts-keys-and-device-identities",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Accounts, keys, and device identities",
    "anchor": "accounts-keys-and-device-identities",
    "text": "User accounts, API keys, SCIM tokens, applications with delegated access, and the identity of devices. The accounts and keys of the operator do not belong here, they cannot be subscribed to (see What never arrives ). Event type Core payload When it is raised identity.user.created userId , tenantId , email , initialRoles A user account was created. identity.user.updated userId , changedFields , newValues Profile data of an account was changed. identity.user.role.changed userId , previousRoles , newRoles The roles of an account were changed. identity.user.suspended userId , reason , cascadeSource An account was suspended. identity.user.deleted userId , deletedById , pseudonymizationJobId An account was deleted, and pseudonymization was triggered. identity.user.pseudonymized userId , pseudonymId , entriesReplaced Entries with personal data in a deleted account were replaced. identity.user.mfa.enabled userId , tenantId , enabledAt The second factor was set up. identity.user.mfa.disabled userId , tenantId , disabledAt The second factor was removed. identity.user.mfa.recovery-codes.regenerated userId , tenantId , regeneratedAt New recovery codes were generated. identity.user.password-changed userId , via , changedAt The password was changed, and via gives the route. identity.api-key.minted apiKeyId , name , keyPrefix , scope An API key was created. identity.api-key.revoked apiKeyId , revokedById , reason An API key was revoked. identity.scim-token.minted scimTokenId , tenantId , to"
  },
  {
    "id": "webhooks/event-types-organization#sessions-and-sign-in",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Sessions and sign-in",
    "anchor": "sessions-and-sign-in",
    "text": "Sign-ins, renewal of sessions, magic links, and actions by platform operations on behalf of an account. sessions.session.theft-detected is the alarm for a reused session token. Event type Core payload When it is raised sessions.login.succeeded sessionId , userId , kind , method A sign-in succeeded, and method gives the route. sessions.login.failed email , reason , ipAddress A sign-in failed. sessions.mfa.verified sessionId , userId , via The second factor was confirmed. sessions.mfa.failed sessionId , userId , attemptCount The second factor was entered incorrectly. sessions.session.refreshed sessionId , familyId , userId A session was renewed (constant traffic). sessions.session.theft-detected sessionId , familyId , replayedHashPrefix A session token that had already been used up was presented again. sessions.session.terminated sessionId , userId , reason A session was ended. sessions.session.all-terminated userId , revokedCount , keptSessionId All sessions of an account were ended. sessions.password-reset.requested passwordResetId , subjectKind , purpose A password reset was requested. sessions.password-reset.completed passwordResetId , userId , subjectKind A new password was set through the reset. sessions.magic-link.requested magicLinkId , email , expiresAt A magic link was requested. sessions.magic-link.consumed magicLinkId , sessionId , userId A magic link was used. sessions.impersonation.started operatorUserId , targetUserId , reason , expiresAt Platform operations act "
  },
  {
    "id": "webhooks/event-types-organization#billing",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Billing",
    "anchor": "billing",
    "text": "Contract, invoices, payment methods, and the unlocked features. A package change reports twice: subscription.package-changed says that the contract is on a different package (on every change), and feature-entitlements.changed says that one of the feature switches has changed, and changedKeys shows which one. There are no separate events for upgrade and downgrade. The direction is in from and to . Event type Core payload When it is raised billing.subscription.created subscriptionId , tenantId , status , billingCycle , currentPeriodEnd A contract was created. billing.subscription.activated subscriptionId , tenantId , from ( trial / past_due ) The contract is active, and from gives the previous state. billing.subscription.past-due subscriptionId , graceUntil , reason ( payment-failed / trial-ended ) Payment did not arrive or the trial ended. graceUntil gives the deadline. billing.subscription.grace-lapsed subscriptionId , tenantId , graceLapsedAt , suspendsAt The deadline has passed, and suspendsAt gives the suspension date. billing.subscription.trial-ending subscriptionId , tenantId , trialEndsAt , daysLeft The trial ends soon, and daysLeft gives the remaining days. billing.subscription.suspended subscriptionId , tenantId The contract was suspended. billing.subscription.canceled subscriptionId , reason The contract was canceled. billing.subscription.package-changed tenantId , from (nullable), to , at The contract is on a different package. It comes on every change, and the dire"
  },
  {
    "id": "webhooks/event-types-organization#joining-and-setup",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Joining and setup",
    "anchor": "joining-and-setup",
    "text": "Invitations, the setup wizard, provisioning of a new organization, join requests, and self-registration. Event type Core payload When it is raised onboarding.invitation.sent invitationId , email , defaultRoleId , expiresAt An invitation was sent. onboarding.invitation.resent invitationId , email , expiresAt An invitation was sent again. onboarding.invitation.revoked invitationId , email , revokedById An invitation was revoked. onboarding.invitation.accepted invitationId , email , acceptedByUserId An invitation was accepted. onboarding.invitation.expired invitationId , tenantId , email An invitation has expired. onboarding.setup-step.completed tenantId , step , via A step of the setup wizard was completed. onboarding.setup-wizard.completed tenantId , completedAt The setup wizard was completed. onboarding.tenant.provisioning-started tenantId , slug , provisionedById Provisioning of a new organization has begun. onboarding.tenant.provisioning-completed tenantId , subscriptionId , ownerEmail Provisioning of a new organization is complete. onboarding.join-request.received joinRequestId , tenantId , email , displayName A join request has arrived. onboarding.join-request.decided joinRequestId , tenantId , email , decision ( accepted / rejected ), decidedById A join request was decided ( accepted or rejected ). onboarding.self-registration.changed enabled , reason , changedById The platform switch for self-registration was flipped (platform operations). onboarding.signup.started funn"
  },
  {
    "id": "webhooks/event-types-organization#related",
    "path": "/webhooks/event-types-organization",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-organization",
    "pageTitle": "Event types: organization, accounts, billing",
    "section": "Related",
    "anchor": "related",
    "text": "Event types: content, programs, devices, apps Create a webhook endpoint What is sent and how it is retried"
  },
  {
    "id": "webhooks/event-types-content-devices",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Event types: content, programs, devices, apps",
    "anchor": "",
    "text": "The subscribable event types from media, scenes, programs, marketplace, fleet, app runtime, playback, analytics, and advertising. Most integrations begin with these events."
  },
  {
    "id": "webhooks/event-types-content-devices#how-to-read-the-tables",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "How to read the tables",
    "anchor": "how-to-read-the-tables",
    "text": "Event type is the string you subscribe to on the endpoint. It arrives in the header X-Event-Type with every delivery. Instead of single types you can also subscribe to a whole area ( composition.* ) or to everything with * . There are no other forms, and the server rejects anything else, see Fetch the event catalog through the API . Core payload names two to four of the most important fields that are in the body of the delivery. It is an excerpt. Each area determines the full shape, which can carry more fields. The time, the event identifier, and the trigger do not travel in the body. When it is raised is a short phrase for context, not a statement about frequency. Under Subscribed events , the portal offers exactly the types of this page and of the page Event types: organization, accounts, billing for selection, the same list that GET /v1/webhooks/event-types returns. Everything else cannot be subscribed to, and creating an endpoint rejects it. Which types these are is described under What never arrives . Only what is raised in the context of your organization is delivered. In the fields, playerId refers to a display (identifier ply_… ) and deviceId to the device it is attached to."
  },
  {
    "id": "webhooks/event-types-content-devices#the-event-types",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "The event types",
    "anchor": "the-event-types",
    "text": ""
  },
  {
    "id": "webhooks/event-types-content-devices#media",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Media",
    "anchor": "media",
    "text": "Files from upload to finished processing, folders, and automatic collections. asset.ready completes the processing of the variants, and asset.deleted triggers the check of the scenes that use the file. Event type Core payload When it is raised media-library.asset.upload-initiated assetId , sha256 , sizeBytes , mimeType An upload was started. media-library.asset.upload-aborted assetId , abortedById An upload was canceled. media-library.asset.uploaded assetId , sha256 , sizeBytes , deduplicated The file is complete, and deduplicated says whether it already existed. media-library.asset.processing-started assetId , tenantId Processing of the file has begun. media-library.asset.ready assetId , variantKinds All variants are generated, and the file is ready for use. media-library.asset.processing-failed assetId , reason Processing failed. media-library.asset.updated assetId , updatedById , changedFields Details of a file were changed. media-library.asset.deleted assetId , deletedById A file was deleted. Scenes that use it are checked. media-library.asset.restored assetId , restoredById A deleted file was restored. media-library.folder.created folderId , path , createdById A folder was created. media-library.folder.moved folderId , oldPath , newPath , affectedDescendants A folder was moved. media-library.folder.deleted folderId , path , cascadeAssetCount A folder was deleted, and cascadeAssetCount counts the files deleted with it. media-library.tag.assigned assetId , tags , assignedB"
  },
  {
    "id": "webhooks/event-types-content-devices#scenes-and-templates",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Scenes and templates",
    "anchor": "scenes-and-templates",
    "text": "Scenes, their versions, and layout templates. scene-version.created is the trigger that playback and programs follow. Event type Core payload When it is raised composition.scene.created sceneId , tenantId , createdById A scene was created. composition.scene.updated sceneId , version , updatedById A scene was edited (every change). composition.scene.published sceneId , sceneVersionId , versionNumber , contentHash A scene was published, and contentHash identifies the state. composition.scene.archived sceneId , archivedById , reason A scene was archived. composition.scene.restored sceneId , restoredById An archived scene was brought back. composition.scene.deleted sceneId , deletedById A scene was deleted. composition.scene-version.created sceneVersionId , versionNumber , contentHash , assetIds A new version of a scene was created, and assetIds lists the files used. composition.layout-template.created layoutTemplateId , createdById A layout template was created. composition.layout-template.updated layoutTemplateId , updatedById A layout template was changed. composition.layout-template.deleted layoutTemplateId , deletedById A layout template was deleted. composition.integrity-issue.detected issueId , sceneId , type , severity A problem in a scene was detected, such as a missing file. composition.integrity-issue.resolved issueId , sceneId , resolution A detected problem was resolved."
  },
  {
    "id": "webhooks/event-types-content-devices#playlists-and-programs",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Playlists and programs",
    "anchor": "playlists-and-programs",
    "text": "Playlists, programs, the assignment to displays, and the plan revisions. schedule-item.changed and player-program.changed are deliberately one collective event each, with an action field. timeline.invalidated gives the cause in reason : timeline-invalidated or media-source-changed (the media selection of an automatic playlist has shifted). plan-revision.recorded records the plan of a display for the next 48 hours. Analytics and advertising calculate the planned time and the target for upcoming hours from it. Event type Core payload When it is raised scheduling.playlist.created playlistId , itemCount , assetIds , createdById A playlist was created. scheduling.playlist.updated playlistId , version , assetIds , updatedById A playlist was changed, and assetIds lists the files it contains. scheduling.playlist.deleted playlistId , deletedById A playlist was deleted. scheduling.schedule.created scheduleId , tenantId , createdById A program was created. scheduling.schedule.updated scheduleId , version , changedFields , updatedById A program was changed. scheduling.schedule.deleted scheduleId , deletedById A program was deleted. scheduling.schedule-item.changed scheduleId , itemId , action ( created / updated / deleted ) An entry of a program was created, changed, or deleted. It is a collective event with action . scheduling.player-program.changed playerId , action ( assigned / cleared / override-set / override-cleared ) The assignment on a display has changed. It is a collective even"
  },
  {
    "id": "webhooks/event-types-content-devices#marketplace-installed-apps",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Marketplace: installed apps",
    "anchor": "marketplace-installed-apps",
    "text": "Apps from the point of view of your organization: installation, update, acceptance and refusal, the requests to the owners, the switch at the end of a phase-out period, uninstallation, and the deletion of the app data afterward. Event type Core payload When it is raised marketplace.app.installed grantId , appId , version , acceptedById An app was installed. marketplace.app.install-requested appId , version , requestedById , message A manager asks the owners to install an app. marketplace.app.upgraded grantId , appId , oldVersion , newVersion An installed app was moved to a new version: silently, after your acceptance, or at the end of a phase-out period. marketplace.app.pending-reacceptance grantId , appId , newManifestHash , addedPermissionKeys A new version requires new permissions, access, or terms, or the installed version is being phased out, and acceptance is pending. marketplace.app.update-decision-requested appId , fromVersion , toVersion , requestedById , message A manager asks the owners to decide on the pending update of an app. marketplace.app.reaccepted grantId , appId , manifestHash , acceptedById An update that was waiting for acceptance was accepted. marketplace.app.update-declined grantId , appId , installedVersion , declinedVersion An update was declined. The app stays on installedVersion until a version higher than declinedVersion appears. marketplace.app.update-withdrawn grantId , appId , installedVersion , withdrawnVersion The version whose acceptance the"
  },
  {
    "id": "webhooks/event-types-content-devices#fleet-devices-displays-locations-video-walls",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Fleet: devices, displays, locations, video walls",
    "anchor": "fleet-devices-displays-locations-video-walls",
    "text": "Devices, displays, commands, tags, locations, video walls, locations, and system updates. There is no event per check-in: player.status-changed is how online and offline show up. The display-group.* events concern the video walls, and their memberPlayerIds lists the affected displays. The ota-* events carry the device identifier ( deviceId ), because a system state belongs to the device, not to the individual display. Event type Core payload When it is raised fleet.device.claimed deviceId , tenantId , claimedByUserId , playerIds A device was assigned to the organization, and playerIds lists the displays that were created. fleet.device.capabilities-changed deviceId , playerIds , capabilities The reported capabilities of a device have changed. fleet.device.channel-changed deviceId , channel , previousChannel , changedById The channel of a device was set, and from now on it follows the releases of the new channel. fleet.player.created playerId , deviceId , screenIndex A display was created. fleet.player.updated playerId , changedFields Details of a display were changed. fleet.player.config-updated playerId , changedConfig The configuration of a display was changed. fleet.player.tagged playerId , tagId A display received a tag. fleet.player.untagged playerId , tagId A tag was removed from the display. fleet.player.status-changed playerId , from , to The status of a display has changed ( from → to ). This is how online and offline show up. fleet.player.deleted playerId , deletedBy"
  },
  {
    "id": "webhooks/event-types-content-devices#app-runtime",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "App runtime",
    "anchor": "app-runtime",
    "text": "Data fetches and recurring jobs of installed apps. fetch.completed is the trigger at which playback picks up new app data. Event type Core payload When it is raised app-runtime.fetch.completed tenantId , appId , fetchKey , payloadHash A data fetch of an app is complete. app-runtime.fetch.failed tenantId , appId , fetchKey , error A data fetch of an app failed. app-runtime.cache.invalidated tenantId , appId , fetchKey Cached app data was discarded. app-runtime.job.scheduled scheduleId , appId , jobKey , cron A recurring app job was scheduled. app-runtime.job.executed scheduleId , appId , jobKey , durationMs An app job was executed. app-runtime.job.failed scheduleId , appId , jobKey , error An app job failed."
  },
  {
    "id": "webhooks/event-types-content-devices#playback",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Playback",
    "anchor": "playback",
    "text": "The computed playback state per display and the reports of the playback software. Only what goes wrong is logged here. The other events are machine traffic whose volume grows with the number of displays. Event type Core payload When it is raised playback.snapshot.recomputed playerId , manifestHash , bundleHashes , recomputeReason The playback state of a display was recomputed. playback.snapshot.bundle-changed playerId , bundleKind , newHash , oldHash A component of the playback state has changed. playback.snapshot.stale-detected playerId , validUntil , ageSeconds The playback state of a display is outdated. playback.snapshot.partial-failure playerId , degradedWidgets Parts of the playback state could not be generated, and degradedWidgets lists them. playback.snapshot.playlist-item-skipped playerId , skippedItems Entries of a playlist were skipped. playback.snapshot.recompute-failed playerId , recomputeReason , error The recomputation of the playback state failed. playback.renderer.diagnostics-ingested playerId , errorCount , severityCounts A display has delivered diagnostic data."
  },
  {
    "id": "webhooks/event-types-content-devices#analytics",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Analytics",
    "anchor": "analytics",
    "text": "Aggregated measurements, key figures, reports, proofs, and the links for advertisers. analytics.proof.issued reaches your endpoint for every proof of your direct business: for closings and addenda from the nightly run as well as for an interim statement that your organization requests. Proofs of the ad network belong to no organization and reach no endpoint. Event type Core payload When it is raised analytics.metric.aggregated metric , timeBucket , bucketStart , entryCount Measurements were aggregated into a time window. analytics.engagement-snapshot.recomputed tenantId , computedAt The key figures of the organization were recomputed. analytics.report.created reportId , tenantId , ownerUserId A report was created. analytics.report.updated reportId , changedFields A report was changed. analytics.report.deleted reportId , tenantId A report was deleted. analytics.report.scheduled-run-triggered reportId , nextRunAt A scheduled report run was triggered. analytics.report.generated generatedReportId , format , rowCount , trigger , window , expiresAt A report was generated: by hand, on a schedule, or as a one-time output ( trigger ). window gives the fixed days, and expiresAt the end of the period in which it can be fetched. analytics.report.generation-failed reportId , error The generation of a report failed. analytics.retention.archived metric , bucketStart , entryCount Old measurements were archived. analytics.proof.issued proofDocumentId , number , kind , campaignId , period , si"
  },
  {
    "id": "webhooks/event-types-content-devices#advertising",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Advertising",
    "anchor": "advertising",
    "text": "Advertisers, ad campaigns with their bookings, creatives and their approval, the ad space of the displays, the audience of the locations, participation in the ad network, and the credit note for the ad revenue of a month. The events are slim, so you read the current state under /v1/advertising/… . The events on advertisers, ad campaigns, bookings, and creatives say in ownerKind and tenantId who owns the row. Only the rows of your organization are delivered ( ownerKind: tenant ). Advertisers, ad campaigns, bookings, and creatives of the ad network carry no organization and reach no endpoint. Event type Core payload When it is raised advertising.advertiser.created advertiserId , ownerKind , tenantId An advertiser was created. advertising.advertiser.updated advertiserId , ownerKind , tenantId The name, brands, industry, contacts, or note of an advertiser were changed. advertising.advertiser.archived advertiserId , ownerKind , tenantId An advertiser was archived. It gets no new ad campaigns, bookings, or creatives, and its ad campaigns and proofs stay readable. Whatever is booked keeps running. advertising.campaign.created campaignId , advertiserId , status An ad campaign was created, and status is draft or booked . advertising.campaign.updated campaignId , advertiserId , status The name, period, goal, price, or note of an ad campaign were changed. A change of state has its own event. advertising.campaign.status-changed campaignId , advertiserId , status An ad campaign was booked"
  },
  {
    "id": "webhooks/event-types-content-devices#what-never-arrives",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "What never arrives",
    "anchor": "what-never-arrives",
    "text": "The catalog under GET /v1/webhooks/event-types lists 241 of the 338 event types of the platform, in 16 areas, exactly the types of this page and of the page Event types: organization, accounts, billing . The other 97 cannot be subscribed to: the events of delivery itself and everything that is raised without a link to an organization. Creating or changing an endpoint rejects such types with 422 WebhookEndpoint.EventUnknown , and there is no area-wide entry such as ops.* for these areas. These are: The events of delivery itself. The twelve types under notifications.* (in-app messages, email sending, and webhook management: notifications.webhook.endpoint.created , .endpoint.disabled , .webhook.delivered , .failed , .dead ) never trigger a webhook, because otherwise every delivery would trigger the next one. You see the disabling of an endpoint in the portal as the status Inactive , not through a webhook. Events of platform operations. The 16 types under ops.* (maintenance windows, incidents, system health, device software) are raised without a link to an organization. The app catalog. The eight types under app-catalog.* (versions, bundles, and status of the apps in the catalog): A version is published once for the platform, not per organization. What follows from it for you is reported by the marketplace.app.* events. Devices before they belong to an organization, and the plumbing of the connection. fleet.pairing.code-issued and fleet.pairing.code-rotated (pairing codes are rai"
  },
  {
    "id": "webhooks/event-types-content-devices#what-to-start-with",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "What to start with",
    "anchor": "what-to-start-with",
    "text": "Four event types let you test most integrations, because you can trigger them yourself in the portal: composition.scene.published (publish a scene), scheduling.playlist.updated (change a playlist), media-library.asset.ready (upload a file), and fleet.player.status-changed (disconnect a display from the network)."
  },
  {
    "id": "webhooks/event-types-content-devices#related",
    "path": "/webhooks/event-types-content-devices",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "event-types-content-devices",
    "pageTitle": "Event types: content, programs, devices, apps",
    "section": "Related",
    "anchor": "related",
    "text": "Event types: organization, accounts, billing Create a webhook endpoint"
  },
  {
    "id": "webhooks/fetch-event-catalog",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "Fetch the event catalog through the API",
    "anchor": "",
    "text": "The catalog of subscribable events is not only in the portal: GET /v1/webhooks/event-types returns it as JSON, with plain-language names, a description, and a complete example body for each event. With it, you check a list of events before the server rejects it and build your own selection without typing names."
  },
  {
    "id": "webhooks/fetch-event-catalog#the-call",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "The call",
    "anchor": "the-call",
    "text": "Parameter Values Default locale de , en de Only these two languages are supported. They are the ones in which the plain texts are complete. A full language code would promise more than exists."
  },
  {
    "id": "webhooks/fetch-event-catalog#what-comes-back",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "What comes back",
    "anchor": "what-comes-back",
    "text": "Field Meaning items[].type the string you subscribe to. It arrives in X-Event-Type with every delivery items[].area the area, that is, the first segment before the dot. This is the value you use when subscribing to a whole area items[].label , .areaLabel , .description plain text for your own interface, in the language from locale items[].exampleBody the complete body of a delivery, not an excerpt. null where no example can be derived from the contract. None is invented areas the areas that have subscribable events, with their plain text headers the names of the three headers that every delivery carries The body of a delivery is the payload of the event itself and nothing around it: the event type and the delivery identifier are in the headers, not in the body. So exampleBody is exactly what your receiver will see."
  },
  {
    "id": "webhooks/fetch-event-catalog#what-you-need-it-for",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "What you need it for",
    "anchor": "what-you-need-it-for",
    "text": "Check before the server rejects. When you create or change an endpoint, the platform checks every entry of the events list. Exactly three forms are allowed: Form Meaning composition.scene.published exactly this event type composition.* every type of the area composition . The area must be in areas * everything that can be subscribed to Everything else is 422 WebhookEndpoint.EventUnknown , and the response lists the names it does not recognize under details.unknownEvents (up to twenty). A typo should stand out on input and not end up as an endpoint that stays silent for years, without a failed attempt and without an entry in the delivery log. Choose without the portal. If you build your own interface, take names, areas, and descriptions from here instead of maintaining them. That way they never go stale. An example for testing. exampleBody is a valid body. Use it to feed your receiver before the event first occurs for real."
  },
  {
    "id": "webhooks/fetch-event-catalog#stability",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "Stability",
    "anchor": "stability",
    "text": "The operation carries x-stability: beta . The shape of this list may still change until it has gone through a cycle without a breaking change. The names of the events themselves are not affected. What beta promises and what it does not is described under Stability classes and sunset ."
  },
  {
    "id": "webhooks/fetch-event-catalog#if-something-goes-wrong",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution 403 on the call The credential is an API key. The webhook permissions are reserved for sessions. Sign in with an account, see Authentication . 422 WebhookEndpoint.EventUnknown An entry in events fits none of the three forms. Check the reported name against items[].type and areas[].area . An expected event is missing from the catalog and cannot be subscribed to The catalog lists only what is raised in the context of your organization. See What never arrives . WebhookEndpoint.EventUnknown is listed with the other webhook codes under Error codes: devices, playback, notifications, operations ."
  },
  {
    "id": "webhooks/fetch-event-catalog#related",
    "path": "/webhooks/fetch-event-catalog",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "fetch-event-catalog",
    "pageTitle": "Fetch the event catalog through the API",
    "section": "Related",
    "anchor": "related",
    "text": "Create a webhook endpoint Event types: organization, accounts, billing Event types: content, programs, devices, apps"
  },
  {
    "id": "webhooks/troubleshooting",
    "path": "/webhooks/troubleshooting",
    "area": "webhooks",
    "areaTitle": "Webhooks",
    "page": "troubleshooting",
    "pageTitle": "When no delivery arrives",
    "section": "When no delivery arrives",
    "anchor": "",
    "text": "The most common reasons why a webhook does not arrive or arrives twice, or why an endpoint cannot be created, in the order in which they occur. Symptom Meaning Solution The endpoint is Active , the delivery log stays empty The subscribed event does not occur in your organization, or the event was added to the list only recently, because a changed list of events takes effect up to an hour later. A misspelled type or one that cannot be subscribed to is not accepted on creation in the first place ( 422 WebhookEndpoint.EventUnknown ) Compare the type literally with the tables: Organization, accounts, billing and Content, programs, devices, apps . Trigger the event in the portal and open the log again The endpoint is Inactive with “last failure …” More than 20 failures in a row have disabled the endpoint. It receives no new deliveries Fix the cause at the receiver, then Activate (or PATCH with {\"active\": true} ). See Disabling an endpoint A delivery is dead although only one attempt was counted Your server responded with 4xx (except 408 and 429). This counts as a permanent failure without a retry For temporary problems, respond with 503 or 429, not with 4xx. Send the affected delivery again through POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry (the portal shows Retry only for Failed ). See The expected response Deliveries fail without a response code, responseCode is null Your server did not respond within 10 seconds, or the connection could not be established Respond with "
  },
  {
    "id": "apps-sdk/index",
    "path": "/apps-sdk",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "index",
    "pageTitle": "Apps SDK",
    "section": "Apps SDK",
    "anchor": "",
    "text": "How an app is built (manifest, widget, backend, data access, signature) and how it gets into the catalog and onto the display."
  },
  {
    "id": "apps-sdk/index#build-an-app",
    "path": "/apps-sdk",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "index",
    "pageTitle": "Apps SDK",
    "section": "Build an app",
    "anchor": "build-an-app",
    "text": "Page When you need it From template to release The whole path of a new app: create it from the template with pnpm app:new , write the widget, config, texts, and images, check it with pnpm app:check , test it, and take it through the pull request into the catalog up to the release in Stable. An app in the new standard Convert an app to templates with size, formats, fit report, surfaces, and the declarative form, using the weather app as an example, plus fit check, store images, old configs, host contract, and the order of the next apps."
  },
  {
    "id": "apps-sdk/index#basics",
    "path": "/apps-sdk",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "index",
    "pageTitle": "Apps SDK",
    "section": "Basics",
    "anchor": "basics",
    "text": "Page When you need it What is an app, and how does it get onto a display? From the manifest through release, channels, and installation to the signed bundle on the device: the lifecycle of an app in one picture. The manifest Every field of the app manifest with whether it is required, its format, and its effect, from id and version to legal, unlocks, and connects. Widget, configuration, and preview defineWidget with configSchema and noResolver, formats (useWidgetFormat), preview variants with size for catalog and thumbnails, and preview states, with the clock example. Configure widgets Read the widget catalog: configSchema as JSON Schema, required fields, examples, and a ready-made widget instance for each built-in building block."
  },
  {
    "id": "apps-sdk/index#backend-and-data",
    "path": "/apps-sdk",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "index",
    "pageTitle": "Apps SDK",
    "section": "Backend and data",
    "anchor": "backend-and-data",
    "text": "Page When you need it Backend: resolvers, fetchers, and jobs The three decorators @WidgetResolver, @Fetcher, and @JobHandler, their rules, and the capabilities that an app’s backend obtains through tokens. Data access, permissions, and terms dataAccess (reads, external, retention), permissions for team and operator, unlocks and connects, the legal block: what an app discloses before installation."
  },
  {
    "id": "apps-sdk/index#publish",
    "path": "/apps-sdk",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "index",
    "pageTitle": "Apps SDK",
    "section": "Publish",
    "anchor": "publish",
    "text": "Page When you need it Signing and the path into the catalog What belongs to a release, sha256 and Ed25519 signature, the policy on the device, channels canary → beta → stable, and who creates releases today. The example apps All 26 bundled apps with profile and special feature, and the three references clock (widget only), weather (fetcher), pharmacies-on-duty (resources and job)."
  },
  {
    "id": "apps-sdk/template-to-release",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "From template to release",
    "anchor": "",
    "text": "After this guide, your app is a version in the Stable channel and therefore in the store of every organization whose package includes it. The path begins with a command that creates the app from a template and ends with the confirmation of a second person in the Ops portal. In between, a second command tells you at any time what is still missing."
  },
  {
    "id": "apps-sdk/template-to-release#the-path-in-one-picture",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "The path in one picture",
    "anchor": "the-path-in-one-picture",
    "text": "The words on this page follow the glossary: the version is the number that an organization reads ( 1.0.0 ), the release is the same version in the operator’s catalog, and the channel says who gets it."
  },
  {
    "id": "apps-sdk/template-to-release#1-create-the-app",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "1. Create the app",
    "anchor": "1-create-the-app",
    "text": "pnpm app:new <id> creates apps/<id>/ from the template and writes nothing else: no list, no entry in any file of the platform. Platform, catalog, screenshots, and budgets find the app through its folder. The identifier is kebab-case and English ( door-sign , not tuerschild ). It applies to the folder, the package @szenera-app/<id> , the manifest, the template’s one widget, and the database schema apps_<id> (with an underscore instead of a hyphen). The command rejects an identifier that is taken, reserved ( core ), or malformed, naming every reason at once, and then creates nothing. The names given under --name (German) and --name-en (English) go into the manifest. Without them, the command temporarily takes a name from the identifier, and you rewrite the texts anyway. pnpm install adds the app to the workspace. Afterward the lockfile is in compact form: pnpm exec prettier --write pnpm-lock.yaml brings it into the tree’s format, and pnpm lizenzhinweise writes the third-party notices with the new state of the lockfile. pnpm --filter @szenera-app/<id> build builds the app. The catalog seed reads its built manifest and skips it with a notice until then. What the template brings: Part Files What is in it Manifest src/manifest/index.ts Identifier and version, texts in German and, under translations , in English, category, class, store block, compatibility (hosts, orientations, behavior without a network), and apiCompatibility.rendererHost from RENDERER_HOST_RANGE Widget src/widgets"
  },
  {
    "id": "apps-sdk/template-to-release#2-write-the-widget-config-and-form",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "2. Write the widget, config, and form",
    "anchor": "2-write-the-widget-config-and-form",
    "text": "config.ts is the value contract of the widget. Every field carries .default() so that an empty config reads completely, and the schema silently discards unknown fields (no .strict() ) so that an older version reads the config of a newer one. The same rule applies to every later version: fields are only added, with a default, and none disappears or changes its type. The baseline records this (step 6). .describe('Label|Sentence') carries the label and help text of a field. The form in config-ui.ts reads both with schemaFieldText() from @szenera/widget-editor/config , so a field has the same name everywhere. Which controls the editor knows is determined by the field kind: Field kind ( kind ) Control in the editor Value in the config text , textarea one line, several lines Text number , slider number, slider Number select , segmented , checkboxGroup selection, segmented control, checkboxes one value, several values from options toggle switch yes or no color color picker #rrggbb date calendar isoDate() : YYYY-MM-DD , empty allowed time time timeOfDay() : HH:mm dateTime calendar, time, and the zone of the entry isoDateTime() : point in time with offset timezone search across all zones with German and English names timezoneName() : the zone’s name ( Europe/Berlin ), empty for the organization’s zone address street, postal code, city, and country as one field postalAddress() location location of the display or a fixed place from the city search, optionally with a fallback place place"
  },
  {
    "id": "apps-sdk/template-to-release#3-write-the-texts",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "3. Write the texts",
    "anchor": "3-write-the-texts",
    "text": "The texts are in German in the manifest, and every additional language is a block under translations that carries only texts. English is required, and defineAppManifest rejects an incomplete block with the path of the missing text. The same rules apply to every text an organization reads, in the store, in the installation dialog, and in the editor: the formal form of address, no semicolon, at most one dash per text, no words from the platform’s inner workings, and no industry reference in an app for all industries. Two entries classify the app: category determines the section in the store and in the widget palette, and tier the class ( standard in every package, pro from the Pro package, addon booked separately). The app’s manifest test records both so that a shifted classification does not pass silently. A class other than standard additionally needs its entry in the sales list EXPECTED_TIERS in services/api/tests/app-tier-classification-sweep.test.ts , for example 'door-sign': 'pro' : it records what is sold, and a test of the API holds every app against it. If the entry is missing, pnpm app:check names it in the Manifest line. An app with an external source or mandatory disclosures additionally carries a legal block ( Data access, permissions, and terms )."
  },
  {
    "id": "apps-sdk/template-to-release#4-icon-and-screenshots",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "4. Icon and screenshots",
    "anchor": "4-icon-and-screenshots",
    "text": "You draw the icon icon.svg yourself. As long as the template’s icon with its data-placeholder marker is there, the Store-Bilder line stays red. The screenshots are produced with the tool that CI also uses: It captures every view under preview.variants , the main view a second time under the name from thumbnailUrl , and the scene example, which is the main view in a typical scene. If a screenshot needs more, such as an accent color of the environment or a fixed partial config, that goes in package.json under szenera.screenshots ."
  },
  {
    "id": "apps-sdk/template-to-release#5-check-with-pnpm-appcheck",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "5. Check with pnpm app:check",
    "anchor": "5-check-with-pnpm-appcheck",
    "text": "The command shows one line per check, with each finding beneath it. It is the same command locally and in CI, and the check before the build runs pnpm app:check --all across all apps. Line What it checks Manifest The manifest loads and satisfies the contract. Folder, package, and manifest have the same identifier, package and manifest the same version, every subpath under exports has its source, and the schema is called apps_<id> . A class other than standard is in the sales list (step 3). Übersetzungen German and English are there. Kompatibilität Hosts, orientations, and behavior without a network are in the manifest. Store-Bilder Icon, every view, the main view, and the scene example are there, each a PNG in the dimensions of its orientation and not empty. Texte Every text from manifest, translation, and form follows the language rule, and every field is named the same in the form as in the schema. Formular Every form description in src/widgets/<folder>/config-ui.ts keeps the rules that no type expresses ( widgetConfigUIIssues ): at most two quick fields of a compact kind, every group once and every field in one that exists, every selectable surface once. Zustände Every widget that fetches data from outside declares the preview state empty ( { id: 'empty', mockData: null } or the empty payload), so that the display shows the quiet surface. An exception until its own wave is listed with a reason in tools/app-kit/src/check/state-exceptions.ts . Grundlinie Test and baseline ar"
  },
  {
    "id": "apps-sdk/template-to-release#6-test",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "6. Test",
    "anchor": "6-test",
    "text": "The tests run against the sources, without a build. tests/manifest.test.ts holds contract, category, terms, hosts, and registry, tests/compat.test.ts the baseline and the host range, and the tests under tests/widgets/ the presentation and form. If you change the config, pnpm app-compat --update door-sign writes the new part into the baseline. If the change breaks the rule from step 2, the command writes nothing, and the test names the field."
  },
  {
    "id": "apps-sdk/template-to-release#7-the-pull-request",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "7. The pull request",
    "anchor": "7-the-pull-request",
    "text": "The PR carries a note : pnpm changeset names the app, and a note for an app needs the section ## Kunde with the sentence that an organization reads for this version. Without a note, the app gets no version. The PR check runs pnpm app:check --all with the rest of the static checks, and at full depth it builds, captures, packs, and checks every touched app the way the delivery path does. Nothing is signed or stored in the PR. After the merge, the version PR bumps the version from the note, and the release train sets the tag @szenera-app/<id>@<version> ."
  },
  {
    "id": "apps-sdk/template-to-release#8-from-the-tag-into-the-catalog",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "8. From the tag into the catalog",
    "anchor": "8-from-the-tag-into-the-catalog",
    "text": "For the tag, release-app.yml builds the version exactly once: build, screenshots, the artifact <id>-<version>.szapp , the check, the signature, and storage in the registry. Then the run reports the version to staging and production, and both fetch the same artifact and create the release in the Canary channel. The same version therefore has the same bytes everywhere. What belongs to a release and how the channels work is covered under Signing and the path into the catalog ."
  },
  {
    "id": "apps-sdk/template-to-release#9-review-and-release-in-the-ops-portal",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "9. Review and release in the Ops portal",
    "anchor": "9-review-and-release-in-the-ops-portal",
    "text": "Open Apps in the Ops portal, the app’s row, and the Releases tab. The new version is in Canary , and Open review leads to the release review. The review shows on the left the store entry as the organizations will see it, on the right the consent sequence, the comparison with the predecessor, “New in this version”, the distribution, and the checklist. If the text for the organizations is missing, enter it here. You may use Promote to Beta … on your own. In Beta , the organizations that have signed up for pre-release versions get the version. After a few days in Beta: Promote to Stable … with a reason. That creates a request, and the version stays in Beta until then. A second person opens the review of the same version and confirms. Only then is the version in Stable , and all organizations whose package includes the app find it in the store."
  },
  {
    "id": "apps-sdk/template-to-release#check-the-result",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "pnpm app:check <id> --built shows every line with ✓ . In the Ops portal, the version is in Stable in the Releases tab, and the history names both people at the release. In the store of an organization with a matching package, the app appears under its category, with icon, screenshots, and scene example from the artifact."
  },
  {
    "id": "apps-sdk/template-to-release#if-something-goes-wrong",
    "path": "/apps-sdk/template-to-release",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "template-to-release",
    "pageTitle": "From template to release",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "What you see What is behind it and what helps app:new creates nothing The message names every reason: a taken or reserved identifier, a form outside kebab-case, a name with quotation marks. Fix the reason and repeat the command. Manifest line: das Manifest lässt sich nicht laden defineAppManifest rejected it, and the text after it names the field, an incomplete translation with its path, for example. Manifest line: EXPECTED_TIERS … nennt … nicht The app has a class other than standard , and the sales list does not know it. Copy the entry from the finding into services/api/tests/app-tier-classification-sweep.test.ts (step 3). Notiz line: CHANGELOG.md nennt die Fassung … nicht The version in package.json has been bumped without the version PR having written it. Only the note bumps versions, through the version PR, and package.json and CHANGELOG.md then change together. Grundlinie line: nicht aktuell The config has a field that the baseline does not know. pnpm app-compat --update <id> records it. Grundlinie line: Das neue Feld hat keine Vorgabe A new field without .default() . A config of an earlier version does not know it and then no longer reads. Store-Bilder line: zu klein A template placeholder or an empty image. The screenshot from step 4 replaces it. vite build breaks at the cap The bundle is larger than szenera.bundleBudgets . First build it slimmer, otherwise raise the cap with a reason in the note field. vite build breaks at the host commitment The runtime bundle impor"
  },
  {
    "id": "apps-sdk/new-app-standard",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "An app in the new standard",
    "anchor": "",
    "text": "A widget rarely fills the whole display in a scene: it is often a small tile, in portrait format at the edge, or a bar. The standard makes sure that it looks good in every frame, that the customer finds a suitable template on insertion, and that the settings window shows what will happen later on the display. This page leads an existing app there step by step, with the weather app as a running example."
  },
  {
    "id": "apps-sdk/new-app-standard#what-the-standard-requires",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "What the standard requires",
    "anchor": "what-the-standard-requires",
    "text": "Building block How to recognize it Step Templates Every variant carries a starting size, exactly one is primary . 1 Formats The arrangement follows the shape and size of the frame, not a setting. 2 Fit report The widget reports what is enabled but does not fit. 3 Surfaces, icons, building blocks Background from the shared surfaces, icons from the shared set, lists, marquee, code, source, and empty state from the display building blocks. 4 Form Described declaratively, with fixed groups, quick fields, and texts from the schema. 5 Custom fields Only where no field kind fits, and then as an individual field. 6 Preview states Rain, night, or “no data” can be toggled in the window, a widget with a source has the empty state, and the display never shows editor text. 7 Fit check Every template and every edge size fits, readable and untruncated, and every state is calm on the display. 8 Store images Every template in its true frame on a calm surface. 9 Old configs A seed step migrates the existing data, and the schema knows only the new fields. 10 Host contract The bundle requires the commitment of the display software that its imports cover and goes out only after it. 11"
  },
  {
    "id": "apps-sdk/new-app-standard#1-templates-with-size-and-primary",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "1. Templates with size and primary",
    "anchor": "1-templates-with-size-and-primary",
    "text": "The variants under preview.variants are the templates on insertion, the images in the store, and the “Template” tiles in the settings window, all from one source. Each carries a starting size in reference pixels of a 1920 × 1080 scene and only the part of the config that defines it. Exactly one is primary : a click on the app in the editor inserts it. The size is a start, not a cage, and the customer then drags the frame as they like. From apps/weather/src/widgets/weather/definition.ts (abridged): What works well: one template per shape that the app supports (tile, wide, tall, bar), and choosing the size so that everything it turns on fits. size.min is the smallest frame in which the widget still shows something useful, and every template must be at least that large. Designs that differ only in content are templates of the same widget, not second widgets."
  },
  {
    "id": "apps-sdk/new-app-standard#2-the-arrangement-follows-the-frame",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "2. The arrangement follows the frame",
    "anchor": "2-the-arrangement-follows-the-frame",
    "text": "useWidgetFormat() from @szenera/widget-foundation/hooks names the frame’s shape ( band , wide , square , tall ), the size class ( compact below 400 reference pixels on the short side, otherwise regular ), and the dimensions. The app draws one arrangement per shape. Nothing is measured in the DOM: the platform knows the frame, so the arrangement is in place on the first draw. The thresholds and the calculation are covered under Formats . The weather app computes the arrangement as a pure function ( runtime/layout.ts ) and calls it from the runtime. From apps/weather/src/widgets/weather/runtime.tsx (abridged): Three rules that every arrangement follows: All dimensions grow with the frame. Calculation is in reference pixels, and type, spacing, and corner radius are fractions of that. That way the same template looks the same in a 4K scene as in Full HD. A fixed drop-out order. What has no room in a frame drops out in an order that the app sets once. In the weather app, place, temperature, and conditions always stay, and then days, hours, and the high and low values drop out. In the column ( tall ), the hours are only added from 1.7 times the height and therefore drop first. Dragging wider never removes content. At the same height, a wider frame never hides more than a narrower one, not even at the boundary between two shapes. A test of the arrangement records this. Writing the arrangement as a pure function pays off: its tests run without a browser and cover every shape and ever"
  },
  {
    "id": "apps-sdk/new-app-standard#3-the-fit-report",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "3. The fit report",
    "anchor": "3-the-fit-report",
    "text": "If the widget hides enabled content, it reports the parts using the keys of the config fields that turn them on. The editor then says beneath the preview and at the toggle why content is missing, instead of it looking like an error. The weather app passes on the list from its arrangement: The complete list is reported at every arrangement, empty as soon as everything is visible, and always, without asking where the widget runs: on the display, the message has no effect."
  },
  {
    "id": "apps-sdk/new-app-standard#4-surfaces-icons-and-building-blocks",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "4. Surfaces, icons, and building blocks",
    "anchor": "4-surfaces-icons-and-building-blocks",
    "text": "The background comes from the shared surfaces in @szenera/widget-foundation/widget-ui/tokens : light , dark , glass , and transparent ( WIDGET_SURFACE_IDS , resolved with resolveWidgetSurface(id) , which returns background and text color). An app may add surfaces of its own, and the weather app has condition , a gradient per weather condition. Custom hex colors in the form are dropped. The config field is a choice among the identifiers: Icons come from the filled set ConditionIcon in @szenera/widget-foundation/widget-ui (sun, fair, cloud, rain, snow, thunderstorm, fog, moon), not from the app’s own graphics. The design is plain: no cards in cards, no borders around tiles, the house font Inter. No app rebuilds what several apps need. The display building blocks in @szenera/widget-foundation/widget-ui carry no style of their own, and they take the surface, font, and dimensions of the app: PagedList , as soon as a widget shows a sequence of similar entries whose number it does not determine (appointments, dishes, headlines). The list pages at a fixed interval instead of pushing the type below the minimum size. QuietState , whenever data is missing. On the display, the surface and at most one neutral sentence, and in the editor, the explanation with the next step. CompanionQr , whenever a code appears. It keeps the quiet zone, contrast, and module size, and the caption never pushes it out of its quiet zone. MarqueeBand for a single line in a bar that is longer than the bar. Paged"
  },
  {
    "id": "apps-sdk/new-app-standard#5-the-form-declaratively",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "5. The form, declaratively",
    "anchor": "5-the-form-declaratively",
    "text": "The settings window builds its form from a description ( defineWidgetConfigUI from @szenera/apps-sdk/config ) that the app registers in its authoring bundle. The groups follow a fixed order so that a customer finds their way in every app: Source, Content, Appearance, Advanced (collapsed). The editor adds Template and Placement itself. From apps/weather/src/widgets/weather/config-ui.ts (abridged): Texts from the schema. Label and help text appear once in .describe('Label|Help text') and reach the form through schemaFieldText . A field has the same name in the editor as in the schema that an assistant reads. Quick fields. quickFields names at most two compact fields that sit on the floating toolbar above the widget, the background in the weather app. The template sits there anyway. Place. The field kind location belongs to placeSelection('display') in the schema: “Display location” or “Fixed place”, and with fallback: true , additionally the substitute place for displays whose location has no known position, in the place field under “Display location”. Whoever reads the location declares the read area player-location and names it in its terms, see Data access, permissions, and terms . Surface. The field kind surface shows tiles with a color swatch per surface so that you recognize it before choosing it. Visibility. showWhen: { field, equals } (or in , inverted with not: true ) hides a field or a whole group as long as it has no effect, for example a color only for a certain sur"
  },
  {
    "id": "apps-sdk/new-app-standard#6-custom-fields",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "6. Custom fields",
    "anchor": "6-custom-fields",
    "text": "If no field kind fits, the app registers an individual field, never a whole form: that way improvements to the window reach every app without it having to catch up. The weather app brings along only the city search itself and attaches it to the path location.place . From apps/weather/src/authoring/index.ts : The field receives value, change callback, and the form context (frame, language, the organization’s displays) as props and renders only itself. Label, group, and help text still come from the description."
  },
  {
    "id": "apps-sdk/new-app-standard#7-preview-states",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "7. Preview states",
    "anchor": "7-preview-states",
    "text": "preview.states are named sample payloads that the window offers for toggling next to “Live data”. That way you see rain, night, or “no data” without waiting for the weather: The payloads sit next to the app’s demo data and follow the same schema as the real payload. A state that the display software does not know shows up in its tests rather than at the customer’s. A widget with a source has the empty state. It shows the widget without data ( mockData: null ), and pnpm app:check requires it. In the weather app: Never editor text on the display. Empty, faulty, or unconfigured means, on the display, a calm surface and at most one neutral sentence. Whoever stands in front of the display cannot change any settings, and in the shop, instructions look like an error. The explanation with the next step belongs in the editor: the app asks useWidgetMode() or uses the QuietState building block, which separates the two. Without data, the weather app shows on the display only “Weather data temporarily unavailable”, and in the editor additionally whether a place, a location, or only the data is missing. The fit check renders every state in display mode and reports a sentence addressed to the editor (step 8 )."
  },
  {
    "id": "apps-sdk/new-app-standard#8-the-fit-check",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "8. The fit check",
    "anchor": "8-the-fit-check",
    "text": "The fit check renders every template in its frame and reports what overflows, is clipped, or is entirely hidden, lines that a truncating box cuts through the letters, texts that overlap each other, type below the minimum size, and what the widget hides in its own frame. It runs as a gate in the pull request. For an app in the new standard, it measures more than the templates: The edge-size set. With size.min , every widget gets the frames at which arrangements break: the minimum size, for each shape the smallest permitted frame and the largest one in the scene, the bar as full width at minimum height, the column as full height at minimum width. Hiding is allowed there, but clipping and too-small type are not. For the weather app (minimum size 160 × 90), that makes eight frames, including the tile 161 × 131, the bar 1920 × 90, and the column 160 × 1080. The minimum font size. Content never below 12, source and as-of lines never below 10 reference pixels. An arrangement that needs space omits content or pages instead of pushing the type down. The source line carries data-fit-meta (the SourceCredit building block is marked as a whole text). Whole texts. Time, date, price, mandatory disclosures, and phone number carry data-fit-whole . Such a text is never shortened, neither with “…” by the box nor by a script. The states on the display. Every preview state in display mode, empty and error in every template, with no sentence addressed to the editor. The images of a frame with a fi"
  },
  {
    "id": "apps-sdk/new-app-standard#9-store-images",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "9. Store images",
    "anchor": "9-store-images",
    "text": "The images in the store show every template in its true frame on a calm surface, with name and dimensions underneath and small templates enlarged. They are produced with the same renderer as the display software: The tool writes apps/<id>/thumbnails/<widget>--<variant>.png per template, a copy of the primary template as <widget>.png , and the scene example. pnpm app:check then checks whether every image is there and not empty. The images belong in the same commit as the templates."
  },
  {
    "id": "apps-sdk/new-app-standard#10-old-configs",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "10. Old configs",
    "anchor": "10-old-configs",
    "text": "Saved scenes carry configs of earlier versions. They are not reinterpreted on reading but written once into the new form by a seed step, existing data and scene templates alike. After that, the schema knows only the new fields, and form, template recognition, API, and assistants see the same state as the display software. The mapping is a pure function with tests over every combination of the old values. In the weather app: a set place becomes the fixed place, the days are on if the old config showed days ( forecastDays above 0 and layout not current-only ), a transparent background stays transparent , and every other one becomes dark . Old keys stay in the JSON. An organization that has not yet accepted the update keeps running with the older version and reads them. The new schema silently discards unknown keys. New fields have a default so that a config without them is also valid. The app’s baseline tests/compat-baseline.json is updated with pnpm app-compat --update <id> as soon as the old fields leave the schema. Scene templates and demo sources are in the new form. A template that writes old fields creates the very case that the seed step has resolved."
  },
  {
    "id": "apps-sdk/new-app-standard#11-host-contract-and-release",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "11. Host contract and release",
    "anchor": "11-host-contract-and-release",
    "text": "Formats, fit report, surfaces, and icons came into the display software with version 1.1.0 of the host contract. An app’s build writes to dist/renderer-host.json which names the bundle takes from the display software, and sets the commitment in the manifest ( apiCompatibility.rendererHost ) to the lowest version that these imports cover. The weather app therefore requires ^1.1.0 , while an app that uses none of the new names stays at ^1.0.0 . A display software with an older commitment rejects a bundle that requires more, as host-incompatible . That determines the order of the release: first the display software with the new commitment through its channels, and only once the devices have accepted it, the app. Fire TV and web display load the display software from the cloud, and a Raspberry Pi fetches it through remote maintenance. Before the release to production comes the visual check on staging on a real device: Pi, Fire TV, and web display, light and dark, every template. If the new version reads data that the organization has not yet consented to, its update waits for that consent, and until then the older version keeps running."
  },
  {
    "id": "apps-sdk/new-app-standard#the-next-apps",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "The next apps",
    "anchor": "the-next-apps",
    "text": "The weather app is the first app in the standard. The order of the rest depends on the benefit for customers and on what the fit check and a look at the store images show today: Order App Why now 1 Opening hours Every location in every industry shows them. Its presentation is manageable, and its four templates already fit. As the second app, it tests the guide with little risk before a large app follows. 2 Pharmacies on duty For pharmacies, mandatory in the shop window and the industry’s core business. It has the two open exceptions of the fit check: in portrait, title and badge overlap with the duty number, and in the template karte the label of a marker disappears below the list. Like the weather app, it gains the most from the display’s location: one scene for all branches, each showing its own duty pharmacies. 3 News with brand apps and ticker One implementation for seven apps (Tagesschau, BBC, BILD, Deutschlandfunk, NDR, SPIEGEL, RSS), so the leverage is large. The marquee line does not grow with the height of its band today, and the ticker clips the descenders of its items on the full stage. Both bars then follow the same shape band . 4 Menu board Food service and bakeries, often in portrait at the counter. The templates fit, but like every app except the weather app, it lacks an arrangement per shape. 5 Calendar The weekly grid barely scales its type and becomes unreadable in small frames. The background is transparent by default, so the frame cannot be seen in the sto"
  },
  {
    "id": "apps-sdk/new-app-standard#related",
    "path": "/apps-sdk/new-app-standard",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "new-app-standard",
    "pageTitle": "An app in the new standard",
    "section": "Related",
    "anchor": "related",
    "text": "Widget, configuration, and preview — every field of the widget definition, formats, fit report, and preview states in detail. From template to release — the whole path of a new app, which already starts in the standard with pnpm app:new . Data access, permissions, and terms — the read area player-location and what an app discloses before installation. Signing and the path into the catalog — channels and release of a version."
  },
  {
    "id": "apps-sdk/about-apps",
    "path": "/apps-sdk/about-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "about-apps",
    "pageTitle": "What is an app, and how does it get onto a display?",
    "section": "What is an app, and how does it get onto a display?",
    "anchor": "",
    "text": "An app is a domain-specific extension of the platform: a manifest, at most one widget, and, if needed, a backend. This page follows its lifecycle from the manifest to the widget in the scene — and says which parts are open to whom today. An app does not start a process of its own. It is a package that the platform loads, and it shows up in four parts. The first is the manifest with identity, widgets, permissions, data access, and signing policy. The second is an optional backend with resolver, fetcher, and jobs. The third is the runtime bundle that the player loads, and the fourth is the authoring bundle with the configuration form for the scene editor. The manifest is the one binding description: what is not stated there does not exist for the catalog, portal, and device. Each app has at most one widget. The app is the unit that an organization installs, evaluates, and removes, and a widget handles several presentations through its preview variants and a configuration field. How much machinery an app carries falls into three levels. Some of the bundled apps render solely from their configuration and have no backend ( clock , webpage , notice-board , countdown , countup , world-clock , word-clock , ticker , qr-code , youtube , stream , embed ). weather , rss , tagesschau , and calendar pull from external sources with a fetcher, and the brand apps bbc , bild , deutschlandfunk , ndr , and spiegel inherit the backend and widget from tagesschau . pharmacies-on-duty , with a fetch"
  },
  {
    "id": "apps-sdk/about-apps#what-follows",
    "path": "/apps-sdk/about-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "about-apps",
    "pageTitle": "What is an app, and how does it get onto a display?",
    "section": "What follows",
    "anchor": "what-follows",
    "text": "Platform operations creates releases. The catalog has no submission and no review queue. Creating, promoting, demoting, and retiring are operator work. Apps are built in the platform’s source tree today. The @szenera/apps-sdk SDK is a package of this source tree and is not published to npm. It does not travel with the services’ release train but has its own cadence. New versions stay compatible. Fields of a widget configuration are only added with a default and never disappear, because a version must read the configurations of its predecessors. An app’s backend runs in exactly one version and serves every version that a display still plays, so its routes, outputs, and responses change only additively, and every route keeps accepting what an earlier version sends. A test in every app holds both against a fixed baseline. There is no submission path for third-party apps. The following pages describe the contract that the bundled apps are written against, not a procedure that you could go through today. The user’s view is in the handbook. Installing, permissions, and terms from the organization’s point of view: Apps in the handbook ."
  },
  {
    "id": "apps-sdk/about-apps#related",
    "path": "/apps-sdk/about-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "about-apps",
    "pageTitle": "What is an app, and how does it get onto a display?",
    "section": "Related",
    "anchor": "related",
    "text": "The manifest — every field with whether it is required, its format, and its effect. Signing and the path into the catalog — bundle, signature, channels. Apps in the handbook — the organization’s view."
  },
  {
    "id": "apps-sdk/manifest",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "The manifest",
    "anchor": "",
    "text": "The manifest is the one binding description of an app: defineAppManifest in the SDK, the check at load time, and the storage in the catalog all read exactly the same shape. The tables list each field with whether it is required, its format, and its effect. Examples follow below."
  },
  {
    "id": "apps-sdk/manifest#the-fields",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "The fields",
    "anchor": "the-fields",
    "text": "Field Required Format Effect id yes Lowercase letters with hyphens ( ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ ). core is reserved and rejected. Canonical identifier of the app. It must match the catalog entry. version yes Semver ( 1.0.0 ) The version that a release freezes. displayName yes 1–60 characters Name in the store and in the portal. description yes max. 300 characters Short description in the store. category yes zeit · information · web-video · gestaltung · geschaeft The single classification: filter in the store and section in the app area of the scene editor. Widgets do not carry one of their own. tier default standard standard · pro · premium · addon Class: standard carries no badge in the store, pro carries the Pro badge, and addon carries the Add-on badge and is booked as an add-on. No app carries premium today. vertical no pharmacy Industry facet of the store. Cross-industry apps omit it. publisher no { name: 1–80 characters } Publisher shown in the store listing. schemaName yes ^apps_[a-z][a-z0-9_]+$ Name of the app database: apps_<id> , with the hyphens of the identifier turned into underscores ( qr-code → apps_qr_code ). Required even without a database. apiCompatibility yes { appsSdk: semver range, apiVersion?: string, rendererHost?: semver range } appsSdk applies to the backend: the loader checks the running SDK version against it and does not load the app if it does not match. rendererHost names the host contract of the display software that the runtime and authorin"
  },
  {
    "id": "apps-sdk/manifest#the-compatibility-block",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "The compatibility block",
    "anchor": "the-compatibility-block",
    "text": "defineAppManifest requires the block both in the type and at runtime. Without it, the function throws with a sentence that names the three fields. Each list names a value at most once. Field Values Meaning hosts szenera-os · fire-tv · tizen · webos · web-display , at least one The hosts the app runs on: SzeneraOS on the platform’s own device, the app for Fire TV and Android, the apps for Samsung Tizen and LG webOS, and a browser without a host of its own. orientations landscape · portrait , at least one The orientations of the display, landscape and portrait, for which the app is suited. offline full · last-known · none What the app shows without a network. full keeps showing everything because the app renders solely from its configuration and the device’s local stock (“static” in the store). last-known shows the state that the server last put into the display’s snapshot. none shows nothing because the app loads live from the network."
  },
  {
    "id": "apps-sdk/manifest#the-translations-block",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "The translations block",
    "anchor": "the-translations-block",
    "text": "The texts of the manifest are written in German, the required language. Each additional language lives under translations as its own block, which carries texts only: displayName and description , store (tagline, long text, features, and setup steps in the order of the German ones), widgets by identifier with name, description, and variants by view, resources by identifier, permissions by key, dataAccess.external in the order of the services, and legal.disclaimerSummary . Identifiers, values, and rules never appear in a block. A block is either complete or absent: defineAppManifest checks it with appManifestTranslationIssues() and throws at build time, naming the location. The store answers in the language that the portal requests and falls back to German. English terms follow the portals: display , never screen , and the video wall is called video wall . pnpm app:check <id> checks the texts of both languages against the house rule, in the Texte line."
  },
  {
    "id": "apps-sdk/manifest#the-legal-block",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "The legal block",
    "anchor": "the-legal-block",
    "text": "Field Required Meaning disclaimerKey yes Key of the version that a consent refers to ( <group>.bedingungen.v1 ). A change to the substance of the notice bumps it ( v1 → v2 ). disclaimerSummary yes, max. 500 characters Short summary in plain text. The installation dialog, the store page, and the widget footer show it unchanged. It represents the version, and rewording it under the same key does not require consent. termsPath no Path of the full text on the public website ( ^\\/[a-z0-9-]+(\\/[a-z0-9-]+)*$ , ≤ 200 characters, e.g., /app-bedingungen/notdienst ). A path, not an address — the portal prepends its base. requireAcceptanceAt default install install : consent at installation and afterward with every new version of the notice. upgrade : consent with every new version of the app, even if the notice is unchanged. never : no consent. A new disclaimerKey makes a release subject to consent: as soon as the new version is published as stable , the installation moves to Update waiting . Anyone who changes the substance of the notice therefore bumps the key, in the same pass as the short summary, the full text, and the app version. The platform silently adopts a reworded short summary under the same key. A manifest change without a version bump does not reach existing installations."
  },
  {
    "id": "apps-sdk/manifest#what-is-not-in-the-manifest",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "What is not in the manifest",
    "anchor": "what-is-not-in-the-manifest",
    "text": "Fetchers and jobs. An app declares them as decorators on classes in its backend ( @Fetcher , @JobHandler , @WidgetResolver ), and the platform discovers them at startup. The manifest remains a pure data format. Whether a load error aborts startup. That is a property of the installation and lives in the server configuration of the operations side. critical in the manifest is only a report."
  },
  {
    "id": "apps-sdk/manifest#example-the-manifest-of-the-pharmacies-on-duty-app-abridged",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "Example: the manifest of the Pharmacies on duty app (abridged)",
    "anchor": "example-the-manifest-of-the-pharmacies-on-duty-app-abridged",
    "text": ""
  },
  {
    "id": "apps-sdk/manifest#form-validation-and-cross-field-rules",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "Form validation and cross-field rules",
    "anchor": "form-validation-and-cross-field-rules",
    "text": "defineAppManifest validates the shape against the schema, applies default values ( tier , critical , criticalForTenant , signing ), requires the compatibility block, and throws on every shape error. Three rules that arise from several fields at once are checked only by the platform at load time: the appId of each widget must equal the id of the manifest, each resource must carry the app’s prefix, and no two widgets may share an identifier. A manifest that contradicts the schema is rejected by release creation with Manifest.ValidationFailed (HTTP 422)."
  },
  {
    "id": "apps-sdk/manifest#related",
    "path": "/apps-sdk/manifest",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "manifest",
    "pageTitle": "The manifest",
    "section": "Related",
    "anchor": "related",
    "text": "Widget, configuration, and preview — the widget definition in the widgets field. Data access, permissions, and terms — dataAccess , permissions , unlocks , connects , and legal in detail. Signing and the path into the catalog — what follows from signing at release and on the device."
  },
  {
    "id": "apps-sdk/widget-and-preview",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Widget, configuration, and preview",
    "anchor": "",
    "text": "defineWidget builds the widget definition that sits under widgets in the manifest: configuration schema, sizes, and the preview data from which the catalog, thumbnails, and the editor’s templates arise. The tables list every field, and the clock example shows them in context."
  },
  {
    "id": "apps-sdk/widget-and-preview#the-fields-of-the-widget-definition",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "The fields of the widget definition",
    "anchor": "the-fields-of-the-widget-definition",
    "text": "Field Required Format Meaning id yes Lowercase letters and digits with hyphens, 2–100 characters The widget type. Under this key the runtime and authoring bundles register their components. appId yes App identifier Must equal the manifest’s id (cross-field rule at load time). name yes 1–60 characters Name in the editor palette. description yes max. 300 characters Short text in palette and store. configSchema yes Zod schema, every field with .default() and .describe() The contract of the widget configuration. defineWidget derives from it the JSON Schema that the catalog serves — see Field texts and examples . noResolver default false boolean true : purely static, renders solely from the configuration. false : requires a @WidgetResolver in the backend, and if it is missing, the app does not load. size.min no { w, h } in whole reference pixels of a 1920 × 1080 scene The smallest frame in which the widget still shows something useful. The editor does not let the handles be dragged below it. Every template must be at least this large. preview.thumbnailUrl yes Path Thumbnail in editor palette and tile. preview.defaultConfig no Configuration Starting values of newly placed instances, and at the same time the basis of every preview variant. preview.mockData no Payload or configuration Sample data for the preview. For static widgets, a representative configuration. preview.variants default [] see below The presentations that the widget supports, which with size are also the templates "
  },
  {
    "id": "apps-sdk/widget-and-preview#field-texts-and-examples",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Field texts and examples",
    "anchor": "field-texts-and-examples",
    "text": "defineWidget translates the Zod schema once , when the manifest loads, into a JSON Schema document according to Draft 2020-12. That is what GET /v1/widget-catalog serves and what a third-party client checks its configuration against. You control two things deliberately here: The field texts. JSON Schema knows two texts per field, title as a short heading and description as a sentence to go with it, while Zod knows only one. Both therefore go into ONE .describe() , separated by a vertical bar: Without the bar, the whole text becomes the description , and the field gets no heading. A field without .describe() appears in the catalog with no explanation, which leaves it practically unlabeled for anyone who works without the editor. The examples. The SDK fills examples itself: the preview.defaultConfig and, for each preview variant, the same configuration with the variant’s partial configuration laid over it. Every example is checked against the Zod schema, and if one breaks its own rule, the app build fails and names the field. A widget without preview.defaultConfig falls back to preview.mockData , and if it has neither, it cannot be built. What the document ends up saying is covered under Configure widgets ."
  },
  {
    "id": "apps-sdk/widget-and-preview#preview-variants",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Preview variants",
    "anchor": "preview-variants",
    "text": "preview.variants is the only source for what an app can show: the store gallery renders its views from it, the image generator derives from it which thumbnails it captures, and the editor offers the variants that have a size as templates. A layout option that is not listed here does not exist in the store. Field Required Format Meaning id yes ^[a-z][a-z0-9-]{1,31}$ , unique per widget Also the name suffix of the image: /apps/<appId>/thumbnails/<widgetId>--<variantId>.png . label yes 1–40 characters Caption of the image (“Cards”, “Next large”). description no max. 160 characters One sentence on what the presentation is good for. config no Partial configuration The difference from preview.defaultConfig . A widget with no choices declares exactly one variant without config . size no { w, h } in whole reference pixels of a 1920 × 1080 scene The template’s starting size: when the editor inserts the variant, the widget gets this frame. Without size , the variant is only an image. aspect default 16:9 , derived with size 16:9 · 9:16 Display orientation of the capture. Landscape is the usual case. With size it follows the size (landscape as long as the frame is at least as wide as it is tall), defineWidget sets it, and a contradicting entry breaks the build. primary default false boolean Exactly one variant per widget that has variants. It becomes the thumbnail in palette and tile and the template that a click inserts. The contract rejects zero or two canonical variants."
  },
  {
    "id": "apps-sdk/widget-and-preview#sizes-on-variants",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Sizes on variants",
    "anchor": "sizes-on-variants",
    "text": "Each variant gets a size that suits its purpose: a clock as a tile ( 600 × 300 ), a news band as a bar ( 1920 × 120 ), a notice board in portrait ( 600 × 800 ), a map view wide ( 1280 × 720 ). The size is the frame with which a click places the template into a 1920 × 1080 scene. On a different stage, the editor converts it via the short side. A variant for portrait displays gets a tall size instead of aspect: '9:16' , and the orientation follows it. No template is taller than 1080 reference pixels, so a portrait frame is a column in the scene and not a whole display. The thumbnail of a variant with a size shows the widget 1 : 1 in this frame, centered on a calm light surface, with name and dimensions underneath. The image stays 1920 × 1080, or 1080 × 1920 for a tall size. A variant without a size fills the stage of its orientation. The fit check renders every variant of every app in its frame and fails if something overflows there, a text is clipped or too small, or the widget hides enabled content through the fit report. It runs as a gate in the pull request and locally like this: The check measures the ink of a text, not its line box. The rules in detail: Rule Finding Overflow An element that draws itself (image, surface, line) goes beyond the frame. Clipped or hidden A text extends out of the frame or out of a box that clips its overflow, or disappears behind it entirely (the last lines of a list with a fixed height). Cut through A box that explicitly truncates ( text-over"
  },
  {
    "id": "apps-sdk/widget-and-preview#preview-states",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Preview states",
    "anchor": "preview-states",
    "text": "preview.states shows in the settings window how the widget looks in a specific situation without that situation actually occurring: rain, night, no data, stale. The editor offers the states as toggles next to “Live data”. Field Required Format Meaning id yes ^[a-z][a-z0-9-]{1,31}$ , unique per widget Identifier of the state. label yes 1–40 characters Caption on the toggle (“Rain”, “Night”). description no max. 160 characters One sentence on what the state shows. mockData yes Payload or configuration The state’s sample data, in the same form as preview.mockData . There is no schema for the payload. defineWidget therefore checks the shape: plain JSON data (no function, no date, no number without a value) and the same kind as preview.mockData (object, list, or single value). The caption is translated under translations.<language>.widgets.<widget>.states.<state>.label , like that of a variant. The empty state is required for every widget with a source ( noResolver: false ), and pnpm app:check demands it in the Zustände line. It shows the widget without data, for a data widget with mockData: null (no payload, as before the first fetch or after a failed one). Apps that redraw their empty state only in their own wave are listed in tools/app-kit/src/check/state-exceptions.ts until then. Never editor text on the display. Empty, faulty, or unconfigured means, on the display, a calm surface and at most one neutral sentence, while the explanation with the next step lives in the editor ( "
  },
  {
    "id": "apps-sdk/widget-and-preview#templates-in-the-editor",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Templates in the editor",
    "anchor": "templates-in-the-editor",
    "text": "In the scene editor, a variant with size is a template. Its content is preview.defaultConfig with the variant’s partial configuration laid flat over it ( { ...defaultConfig, ...variant.config } ), exactly as in the image generator. The editor therefore inserts what the store shows, in exactly this way. Action What happens Click on the tile in the app area The canonical template ( primary ) lands centered on the stage at its starting size. If a slot of the layout template is free, the widget fills it with the template whose shape fits the slot best: the tall template in a column, the bar in a bar. Arrow on the tile “Insert ” shows all templates as live thumbnails in the true aspect ratio, with name, shape, and size. A click inserts exactly that one. Hovering over the tile The preview shows the canonical template in its frame, not filling the stage. Dragging onto the stage Starting size of the canonical template around the drop point, with the free slot under the pointer highlighted. Toolbar on the selected widget “Template” separates what fits the frame (same shape, only the content changes) from what sets a new frame (centered on the old center point). In a slot, only the content changes. Settings window Tiles per template, the active one marked, “customized” as soon as the content deviates. Resetting returns to the active template. A selected template sets the frame only when you apply it, together with the config in one step. Three rules that an app should know when designi"
  },
  {
    "id": "apps-sdk/widget-and-preview#one-widget-per-app-templates-instead-of-second-widgets",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "One widget per app, templates instead of second widgets",
    "anchor": "one-widget-per-app-templates-instead-of-second-widgets",
    "text": "No app carries two widgets. The app is the unit that an organization installs and removes again, and a bundle of unrelated widgets could not be evaluated. Anything that needs several presentations is solved through the preview variants and a configuration field: A variant with size is a template. It fixes the frame’s starting size and, with its partial configuration, the content the customer gets on insertion. How the content looks inside the frame is decided by frame and configuration. The widget reads the frame’s shape through useWidgetFormat() , and the customer’s choice sits in a configuration field such as composition for the clock. A widget with noResolver: false is also resolved without a player, for example for the preview in the editor. The resolver then receives playerId: null and has to handle that case."
  },
  {
    "id": "apps-sdk/widget-and-preview#formats-how-a-widget-uses-its-frame",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Formats: how a widget uses its frame",
    "anchor": "formats-how-a-widget-uses-its-frame",
    "text": "A widget rarely fills the whole display in a scene. It is often small, in portrait, or a bar. It therefore arranges itself according to the frame the scene gives it, using one calculation for all apps. The hooks live in @szenera/widget-foundation/hooks : Hook Returns Purpose useWidgetFormat(options?) { shape, size, aspect, shortSideRef, width, height } Shape and size class of the frame. The app draws one arrangement per shape. useWidgetGeometry() { x, y, width, height, stage: { width, height } } The resolved box in stage pixels and the stage, without DOM measurement. useFitReport() { reportHidden(parts) } Tells the editor which enabled parts are currently missing in the frame. The shape follows the aspect ratio width : height: Shape Aspect ratio Typical place band from 3.2 : 1 Bar at the edge of the scene wide 1.3 to 3.2 Horizontal box square 0.78 to 1.3 Tile tall below 0.78 Column When dragging the handles in the editor, the shape changes only once the ratio is 4% past the threshold. That keeps a widget from jumping between two arrangements with every pointer movement. The size measures the short side of the frame in reference pixels of a stage with a short side of 1080: short side × 1080 / short side of the stage . Below 400 reference pixels, the frame is compact , otherwise regular . A 4K scene has twice as many pixels, so its widget is not larger: 600 pixels on a stage 2160 high are 300 reference pixels. If you want finer steps, pass your own steps, for example useWidgetF"
  },
  {
    "id": "apps-sdk/widget-and-preview#display-building-blocks",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Display building blocks",
    "anchor": "display-building-blocks",
    "text": "What several apps need lives once in @szenera/widget-foundation/widget-ui . The building blocks carry no style of their own, and they take the surface, font, and dimensions of the app. They arrived in the display software with version 1.2.0 of the host contract, so an app that uses one of them requires ^1.2.0 (see Host contract and release ). Building block Purpose When to use PagedList Entries on pages instead of shrinking type: the list keeps to the minimum font size, distributes what does not fit beyond that across pages at a fixed ten-second interval with a page indicator “1/3”, and shows the first page with motion: frozen . Each entry is measured in an invisible measuring layer. As soon as a widget shows a sequence of similar entries whose number the app does not determine: appointments, dishes, headlines, pharmacies on duty. The app arranges a fixed number of parts itself. QuietState On the display, the surface and at most one neutral sentence, and in the editor, the explanation with the next step. Whenever a widget has no data: source empty, fetch failed, required field missing. The empty state shows exactly this building block. CompanionQr A code with a caption that scans on displays: quiet zone of four modules, a code area that is never transparent, module contrast, whole pixels per module, error correction M. The caption never pushes the code out of its quiet zone. Whenever an app shows a code, as the target of a link, a route, or a guest Wi-Fi. MarqueeBand The marq"
  },
  {
    "id": "apps-sdk/widget-and-preview#the-two-bundles",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "The two bundles",
    "anchor": "the-two-bundles",
    "text": "Bundle Entry point What it registers runtime (player) registerWidgets(registry) Per widget type the render component, loaded lazily: registry.register('clock', lazy(() => import('../widgets/clock/runtime'))) . authoring (editor) registerAuthoring(registry) Per widget type the configuration form: registry.registerConfigForm('clock', ClockConfigForm) . The renderer loads the runtime bundle from the device and has it enter its widgets into the registry. If the import fails, it reports widget.bundle.load-failed instead of aborting the scene. The portal’s scene editor loads the authoring bundle."
  },
  {
    "id": "apps-sdk/widget-and-preview#example-the-clock-abridged",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Example: the clock (abridged)",
    "anchor": "example-the-clock-abridged",
    "text": "The configuration schema for it is a Zod object in which every field carries a default and a text, for example variant: z.enum(['digital', 'analog']).default('digital').describe('Variante|„digital\" ist die Zahlen-Uhr, „analog\" ein gezeichnetes Zifferblatt.') . The schema silently discards unknown keys so that older saved scenes continue to parse."
  },
  {
    "id": "apps-sdk/widget-and-preview#related",
    "path": "/apps-sdk/widget-and-preview",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "widget-and-preview",
    "pageTitle": "Widget, configuration, and preview",
    "section": "Related",
    "anchor": "related",
    "text": "The manifest — where the widget definition is entered. Backend: resolvers, fetchers, and jobs — the resolver that noResolver: false requires. The example apps — category, class, and terms of each app. Signing and the path into the catalog — how the finished definition comes back out through GET /v1/widget-catalog . Configure widgets — how a client reads the generated schema and checks its configuration against it."
  },
  {
    "id": "apps-sdk/configure-widgets",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Configure widgets",
    "anchor": "",
    "text": "The widget catalog delivers a complete JSON Schema of its configuration for every widget: each field with a heading and an explanation, the required fields named, and at least one ready-made example configuration. Anyone who builds a scene without the editor reads the rules from it instead of guessing them — and checks their configuration against it before writing it."
  },
  {
    "id": "apps-sdk/configure-widgets#fetch-the-catalog",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Fetch the catalog",
    "anchor": "fetch-the-catalog",
    "text": "The response carries one entry per widget under entries : Field Meaning appId The app that the widget belongs to, or core for the six built-in building blocks. version The version of the app that the definition comes from. Fixed at 0.0.0 for the built-in building blocks. definition.id The widget type. This exact value later appears in widgetType of the widget instance. definition.name , definition.description Name and short text, as the selection in the editor also shows them. definition.configSchema The JSON Schema of the configuration — the subject of this page. definition.size.min The smallest frame, in reference pixels of a 1920 × 1080 scene, in which the widget still shows something useful. definition.preview.defaultConfig The configuration with which a freshly inserted instance starts. definition.preview.variants The presentations that the widget supports, each with a partial configuration and, as a template, a starting size ( size ). definition.preview.states Named sample payloads for the preview in the editor (rain, night, no data). Of no significance for a configuration. The catalog mirrors the latest released state of an app. An organization that has installed an older version may differ slightly from it, and the version that counts is then the one in the instance’s appVersionPin ."
  },
  {
    "id": "apps-sdk/configure-widgets#what-is-in-the-configschema",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "What is in the configSchema",
    "anchor": "what-is-in-the-configschema",
    "text": "The configSchema is a JSON Schema document according to Draft 2020-12. It describes exactly the object that later appears as config in the widget instance. Key Meaning $schema The version of the specification: https://json-schema.org/draft/2020-12/schema . type Always object — a widget configuration is never a list and never a bare value. title , description Name and short text of the widget, so that the document stays understandable on its own. properties The fields of the configuration, one node per field. required The field names without which the configuration is incomplete. Often empty: fields with a default are not required. examples At least one complete configuration that you can adopt unchanged. Each node under properties carries plain text and, where they exist, the limits of the value: Key Meaning title Short heading of the field, as it also appears above the control in the editor. description One sentence on what the field does. type string , number , integer , boolean , array , or object . enum The permitted values of a selection field — the check rejects any others. default The value that applies if the field is missing. minimum , maximum , minLength , maxLength , pattern The limits of the value. pattern usually appears for colors ( #rrggbb ) and identifiers. properties , items For nested objects and lists: the same rules one level deeper. An excerpt from the “Text” building block:"
  },
  {
    "id": "apps-sdk/configure-widgets#four-rules-you-cannot-see-in-the-schema",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Four rules you cannot see in the schema",
    "anchor": "four-rules-you-cannot-see-in-the-schema",
    "text": "Required means: there is no default. required lists only what the platform cannot fill in itself, usually the content. Everything else carries a default , and an omitted field takes exactly that value. Unknown fields are discarded, not rejected. A field that the schema does not know does not make a configuration invalid, and it disappears on saving. That keeps scenes created before a field restructuring readable, but do not count on a typo being noticed. The examples are complete and verified. Every entry in examples is a configuration that satisfies the schema: the starting configuration and one more per preview variant. An app build aborts if an example breaks its own rule. A configuration survives new versions. An app adds fields only with a default, and none disappears or changes its type. A stored configuration therefore stays valid when the organization updates the app, and an older version that a display still plays also reads a configuration with fields from a newer one."
  },
  {
    "id": "apps-sdk/configure-widgets#two-values-with-a-fixed-shape-place-and-surface",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Two values with a fixed shape: place and surface",
    "anchor": "two-values-with-a-fixed-shape-place-and-surface",
    "text": "Two field kinds of the editor forms write values whose shape is the same everywhere. Anyone who builds a configuration themselves finds them in the configSchema as an object or a string, respectively: Value Shape Meaning Place ( placeSelection() ) { \"mode\": \"display\" | \"fixed\", \"place\": Place | null, \"fallback\": Place | null } , a place is { \"displayName\", \"countryCode\", \"latitude\", \"longitude\" } display takes the location of the display that plays the scene, and fixed takes the place under place . fallback is the substitute place for displays whose location has no known position. Default: display , both places null . Surface an identifier such as light , dark , glass , transparent , or one of the app’s own The background of the widget. The permitted values are named by the field’s schema. How the editor shows these and all other fields, organizes them, and places them on the widget toolbar is described by the app’s form description ( config-ui.ts , type WidgetConfigUI ). It belongs to the editor, not to the catalog: the configSchema remains the single rule for everyone who writes a configuration."
  },
  {
    "id": "apps-sdk/configure-widgets#the-six-built-in-building-blocks",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "The six built-in building blocks",
    "anchor": "the-six-built-in-building-blocks",
    "text": "Text, image, video, shape, graphic, and document are part of the core setup: they are not installed, need no consent, and are open to every organization. In the catalog they appear like the widgets of an app, with the reserved identifier core and the fixed version pin 0.0.0 . widgetType Name Purpose text Text A text from the configuration, without markup and without network access. image Image An image from the media library, optionally with slow image motion. video Video A video from the media library, muted and looping. shape Shape Rectangles, circles, stars, arrows, and lines, drawn from numbers. graphic Graphic A motif from the built-in library, tintable and flippable. document Document A PDF from the media library as page images: one page, in sequence, or two side by side. Image, video, and document refer to the media library instead of an address: assetRef carries the identifier of the file and the file variants that the display software needs. Without assetRef , the widget shows a designed placeholder — that is a valid state and not an error. For the document, assetRef.variantSet stays empty: its page images always travel along, one row page-<n> per page in the snapshot’s media list. Which pages the widget shows is stated in pages : numbers are the chosen pages, and of is the file’s page count at the time of choosing. If pages is missing, the widget shows all pages except empty ones. If the file later has a different page count than of , the scene check reports Documen"
  },
  {
    "id": "apps-sdk/configure-widgets#a-ready-made-widget-instance-per-building-block",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "A ready-made widget instance per building block",
    "anchor": "a-ready-made-widget-instance-per-building-block",
    "text": "This is what an entry in a scene’s document.widgets looks like. id is a new identifier in the format wgi_<ULID> , frame is the box in pixels of the scene surface, and zIndex is the stacking order. Text Image Video Shape Graphic Document A shorter form works too: because every field except content carries a default, { \"content\": \"Willkommen\" } is already a valid text configuration. The spelled-out form above is the one that the editor saves."
  },
  {
    "id": "apps-sdk/configure-widgets#check-a-configuration-before-you-write-it",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Check a configuration before you write it",
    "anchor": "check-a-configuration-before-you-write-it",
    "text": "Take the configSchema of the widget you want and give it to a validator for Draft 2020-12. The result is a field-level message instead of a rejected request. The validator does not cover three things: Whether the file that assetRef points to exists. The media library answers that. Whether the widget type belongs to an installed app. The platform rejects widgets of an app that is not installed when publishing, and the built-in building blocks are always open. Whether the configuration makes sense. A QR code with an empty address is schema-valid and still shows only a placeholder."
  },
  {
    "id": "apps-sdk/configure-widgets#the-form-in-the-editor",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "The form in the editor",
    "anchor": "the-form-in-the-editor",
    "text": "This section is aimed at app authors: it describes how the scene editor builds the settings window and the widget toolbar from an app’s form description ( config-ui.ts ). It changes nothing for a configuration written through the API."
  },
  {
    "id": "apps-sdk/configure-widgets#the-order-of-precedence",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "The order of precedence",
    "anchor": "the-order-of-precedence",
    "text": "The settings window takes the app’s form description if it has one registered ( registry.registerConfigUI(widgetType, ui) ). Only without it does an app’s own form apply ( registerConfigForm ), and without both, one derived from the schema. Declarative comes first so that shared improvements (structure, collapsed groups, hints, place field) reach every app without it having to update its form. An app in the new standard therefore no longer registers a whole form, only individual fields at most."
  },
  {
    "id": "apps-sdk/configure-widgets#custom-fields-registerconfigfield",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Custom fields: registerConfigField",
    "anchor": "custom-fields-registerconfigfield",
    "text": "What no field kind covers (a city search, a selection from its own data), the app brings along as an individual control: The key is the dot path of the field ( city ) or of a part of it ( location.place is the place selection of the place field location ). The form renders label, help text, required asterisk, and error around the control. It receives: Prop Content value , onChange The value at this key and the way to write it. field The declared description of the field. config The whole config of the draft, read-only. error The message if the draft violates the schema here. context Frame of the widget in stage pixels, its shape and size class ( band , wide , square , tall , the same classification as useWidgetFormat ), language and time zone of the organization, and its displays (name, location name, whether the location has coordinates). apiFetch The app’s transport for its own routes, as with a form of its own."
  },
  {
    "id": "apps-sdk/configure-widgets#field-kinds-with-a-control-of-their-own",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Field kinds with a control of their own",
    "anchor": "field-kinds-with-a-control-of-their-own",
    "text": "Field kind In the window On the toolbar location Choice “Display location” or “Fixed place” (without a display reference using allowDisplay: false , only the fixed place). With the display reference, a sentence on which displays have no location with an address, and with fallback: true , a substitute place. The place selection is the app’s own ( <field>.place ), otherwise a simple input of name, country, latitude, and longitude. not compact, window only surface Tiles with a color swatch and label, one active. Selection with swatch Group with collapsed: true Accordion, closed on opening, and open as long as an error is inside. – required: true Required asterisk on the label. The schema decides whether a value is missing. –"
  },
  {
    "id": "apps-sdk/configure-widgets#quick-fields-quickfields",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Quick fields: quickFields",
    "anchor": "quick-fields-quickfields",
    "text": "quickFields names at most two fields of a compact kind ( color , segmented , select , slider , toggle , surface ), in the order of the toolbar. The toolbar leaves out other kinds and more than two fields, and defineWidgetConfigUI() reports this at build time. Besides the quick fields, the toolbar shows the template selection for a widget with several templates ( Widget, configuration, and preview )."
  },
  {
    "id": "apps-sdk/configure-widgets#the-fit-report-keys-are-field-names",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "The fit report: keys are field names",
    "anchor": "the-fit-report-keys-are-field-names",
    "text": "If the runtime hides enabled content because the frame is too small, it reports it with useFitReport().reportHidden(parts) . The parts are named like the config fields that turn the content on, for example ['showDaily'] . The editor then shows “Hidden in this frame: Next days.” beneath the preview, using the field’s label from the form description, and beneath the toggle a sentence on why it has no effect. A part without a matching field appears with its key."
  },
  {
    "id": "apps-sdk/configure-widgets#related",
    "path": "/apps-sdk/configure-widgets",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "configure-widgets",
    "pageTitle": "Configure widgets",
    "section": "Related",
    "anchor": "related",
    "text": "Widget, configuration, and preview — how an app writes its widget definition, from which this schema arises. Signing and the path into the catalog — the two catalogs across all apps and their queries. Data access, permissions, and terms — what an app discloses before installation."
  },
  {
    "id": "apps-sdk/backend",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "Backend: resolvers, fetchers, and jobs",
    "anchor": "",
    "text": "An app’s backend is a module of classes that the platform loads into a container of its own at startup. Three decorators turn it into behavior: @WidgetResolver , @Fetcher , and @JobHandler . Everything the backend needs from the platform it obtains through tokens. This page covers the contracts, rules, and limits."
  },
  {
    "id": "apps-sdk/backend#what-the-backend-is",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "What the backend is",
    "anchor": "what-the-backend-is",
    "text": "Only data apps export a backend. Apps without behavior omit it instead of maintaining an empty module. The platform does not import the module but moves its classes into a separate container for each app, in which the tokens are bound. The backend’s own routes live under /v1/apps/<id>/… . Two apps can never inject classes into each other, and a backend sees only the surface of the SDK. All three decorators only leave a marker on the class. The platform reads them at startup, registers resolvers, creates workers for fetchers and jobs, and wires them into the snapshot build. Each class gets exactly one marker per kind. The class additionally carries the injection markers of the platform container ( @Injectable on the class, @Inject(Token) on every constructor parameter) — without them the container cannot create the class."
  },
  {
    "id": "apps-sdk/backend#widgetresolver",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "@WidgetResolver",
    "anchor": "widgetresolver",
    "text": "An app registers exactly one resolver per widget type. During the snapshot build, the platform calls resolve(ctx) with the already validated configuration and receives a render-ready payload — or null . The weather app’s resolver, abridged: error handling and time-window calculation are left out. It never calls the source itself: it determines the place (a fixed choice or the display’s location from ctx.location ), reads through the fetcher cache ( APP_FETCHER_CACHE ), and passes the fetcher in only as a loader for the cache miss. Two details from the unabridged version: an error from the provider or the infrastructure is logged as weather.resolve.failed and rethrown so that the snapshot build reports it as a resolver error rather than as “no result.” And on a cache hit, the time windows are anchored to the original fetch time ( cachedAt ), not to “now” — zeitzusagen from @szenera/apps-sdk/backend computes validUntil and cacheUntil from it. The context (WidgetResolverContext): Field Type Meaning tenantId string The organization being resolved for. playerId string or null The player. null for the on-demand preview (in the editor, for example) as long as it does not resolve for a specific display. widgetInstanceId string The instance in the scene. widgetType string The widget type. appVersionPin string The app version pinned in the scene version, e.g., 1.2.0 . config unknown Already validated against the configuration schema. traceId string Identifier for tracing. mapTiles { li"
  },
  {
    "id": "apps-sdk/backend#fetcher",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "@Fetcher",
    "anchor": "fetcher",
    "text": "@Fetcher marks a method that fetches an external source. The platform creates a worker of its own for it, which executes only one run at a time so that limited external sources are spared. Option Format Meaning key Pattern with app prefix, e.g., weather:{citySlug} Key in the cache. The first segment before the colon is the app identifier. defaultValidity ISO 8601 duration, e.g., PT30M How long the data counts as fresh. defaultCacheUntil ISO 8601 duration, e.g., PT2H How long the cache keeps serving it as a fallback. The method returns FetcherResult<TPayload> : payload , validUntil , cacheUntil , and source (origin, e.g., api:openweather ). Scheduled runs and refreshing through the API call a fetcher method without arguments . If it declares required parameters, the platform treats it as a pure cache loader: scheduled runs and refreshing reject it, and only the app itself calls it, from getOrFetch in the resolver or from a job that knows the arguments. Periodic fetching is therefore a matter for a @JobHandler . getOrFetch checks on every path, even on a cache hit, whether the organization has the app actively installed, and otherwise the platform answers with App.NotInstalled (HTTP 409). The two time windows result in three situations: Situation What happens now ≤ validUntil serve, nothing else validUntil < now ≤ cacheUntil serve immediately and refresh in the background now > cacheUntil the caller waits synchronously for the source If the refresh fails, the stored entry remai"
  },
  {
    "id": "apps-sdk/backend#jobhandler",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "@JobHandler",
    "anchor": "jobhandler",
    "text": "@JobHandler marks a method as a recurring, platform-wide job. The platform creates a schedule that a cron works through and calls the method without an organization in the context. The handler fans out across the installed organizations itself. Option Format Meaning key string, e.g., pharmacies-on-duty-crawl-all-locations Unique within the app, without app prefix. cron 5 fields ( min hour dom month dow ), e.g., */30 * * * * The schedule. namespace string, default app identifier The platform derives the app from namespace , and the bundled apps set it equal to the app identifier. timeout ISO 8601 duration, e.g., PT5M Hard limit per run. concurrency number, default 1 Simultaneous runs. listTenantsForApp() on the job queue token supplies the organizations, only those with an active installation of this app, and another app cannot be enumerated. A key that is already registered as a fetcher is skipped with a warning."
  },
  {
    "id": "apps-sdk/backend#the-tokens",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "The tokens",
    "anchor": "the-tokens",
    "text": "The backend obtains every cross-cutting capability through a token, and behind it the platform binds an implementation that automatically scopes everything with the app identifier. Three tokens carry context, and the rest provide services. Token What you get Scoping by the platform APP_ID The app identifier — APP_MANIFEST The parsed manifest — APP_VERSION The version from the manifest — Token of the app database (named after the database tool in the SDK) The app database: models , $transaction(fn) Only its own tables in the schema apps_<id> . Without a schema of its own, access fails. APP_PERMISSION_GUARD assert(actor, action, resource?) , canAct(…) Permission keys prefixed with the app identifier. APP_REQUEST_SCOPE traceId , correlationId , actorId , tenantId , isPlatformContext Read-only. APP_ERROR_FACTORY businessException , notFound , validationFailed , conflict , integrationFailed Supplies the error that you throw. APP_EVENT_BUS emit(tx, eventName, payload) , contentChanged(tx, { tenantId, locationIds? }) Event name app.<id>.<eventName> . You pass the transaction explicitly. contentChanged is the fixed event app.<id>.content.changed , see Reporting changed data to the displays . APP_LOGGER info , warn , error , debug , child Namespace app.<id> . APP_CLOCK now() , nowMs() Time source, replaceable in tests. APP_CACHE get , set , invalidate Keys app:<id>:… . APP_FETCHER_CACHE getOrFetch(key, fetcher, { ttl, staleWindow? }) , invalidate Read-through cache. The organization c"
  },
  {
    "id": "apps-sdk/backend#appguardedfetch-is-required-for-user-addresses",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "AppGuardedFetch is required for user addresses",
    "anchor": "appguardedfetch-is-required-for-user-addresses",
    "text": "As soon as a backend fetches an address that someone entered in the portal (a feed, a calendar, a news source from a widget configuration), it must use fetchText , never the global fetch . The guard lets only http: and https: through, checks all resolved addresses against the blocklist of private and reserved ranges, repeats the check on every redirect, truncates the response in the stream, and enforces a timeout. Defaults: timeoutMs: 10000 , maxBytes: 2097152 (2 MiB), maxRedirects: 3 . Every failure carries a machine-readable reason : invalid-url , blocked-scheme , blocked-address , dns-failure , too-many-redirects , too-large , timeout , http-error , network-error . isGuardedFetchError recognizes it structurally, so a resolver can put the reason into its payload as plain text. The guard is not intended for a provider hard-wired in the code (the weather API, for example), because no user input chooses the target there."
  },
  {
    "id": "apps-sdk/backend#reporting-changed-data-to-the-displays",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "Reporting changed data to the displays",
    "anchor": "reporting-changed-data-to-the-displays",
    "text": "If something changes that a widget already shows, the backend reports it with contentChanged . The platform then rebuilds the organization’s displays whose program contains the app and notifies those whose content has actually changed. The displays show the change after a few seconds. Without the report, the change arrives only when the snapshot is rebuilt for another reason, in the worst case after hours. Rule Why In the same transaction as the change The report goes through the outbox: it arrives at least once, but never for a change that was rolled back. Without a database of its own, null instead of the transaction An app that keeps its data in file storage reports right after writing with contentChanged(null, { tenantId }) . The report then goes into the outbox alone. If it fails, the change remains, and the displays see it with the next expiry of their payload. After changing and deleting, not after creating A resolver reads data through the identifier that a widget has chosen. No widget points to a new entry yet, and whoever puts it into a scene publishes the scene, which triggers the rebuild anyway. Always provide tenantId Jobs run without an organization in the context. In the context of an organization, the platform rejects a different one. locationIds only if the change becomes visible exclusively at these locations The identifiers are locations of the organization, not records of the app. If a widget can also show the data on a display elsewhere (a fixed location "
  },
  {
    "id": "apps-sdk/backend#installing-and-uninstalling-the-lifecycle-hooks",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "Installing and uninstalling: the lifecycle hooks",
    "anchor": "installing-and-uninstalling-the-lifecycle-hooks",
    "text": "A backend class marked with @AppLifecycle() may carry onInstall(tenantId) and onUninstall(tenantId) : at most one class per app, and both hooks must be idempotent because a reinstallation finds its existing data again. If onInstall throws, the installation counts as failed and the organization sees the error. If onUninstall throws, the uninstallation remains in effect and the error lands in the log."
  },
  {
    "id": "apps-sdk/backend#testing-without-a-running-platform",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "Testing without a running platform",
    "anchor": "testing-without-a-running-platform",
    "text": "createMockAppContainer from @szenera/apps-sdk/testing supplies an in-memory implementation with recording for each token. mockWidgetResolverContext builds a synthetic context, initially without location . An app with player-location sets it with mockWidgetLocation() or null ."
  },
  {
    "id": "apps-sdk/backend#related",
    "path": "/apps-sdk/backend",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "backend",
    "pageTitle": "Backend: resolvers, fetchers, and jobs",
    "section": "Related",
    "anchor": "related",
    "text": "Data access, permissions, and terms — what dataAccess.reads binds to APP_TENANT_DATA . Widget, configuration, and preview — the widget that the resolver serves. The example apps — which app carries which building blocks."
  },
  {
    "id": "apps-sdk/data-access",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "Data access, permissions, and terms",
    "anchor": "",
    "text": "Four blocks of the manifest disclose what an app does with data, who in the team may operate it, what it unlocks in the portal, and which terms an organization agrees to: dataAccess , permissions , unlocks with connects , and legal . For each block, the tables give the format, limits, and the effect in the portal."
  },
  {
    "id": "apps-sdk/data-access#four-blocks-four-questions",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "Four blocks, four questions",
    "anchor": "four-blocks-four-questions",
    "text": "Block The organization’s question Where the portal shows it dataAccess What does the app do with my data, before and after installation? The Data & access section of the store page and the installation dialog permissions Who in my team may operate the app? Roles & permissions — collapsed at the end of the store page, spelled out in the installation dialog unlocks , connects What does the app unlock in the portal, and which external account does it need? Unlocks in the portal legal Which terms do I agree to? Important note in the installation dialog, store page, footer of the widget The blocks are deliberately separate: permissions describes who in the team may operate the app, not what the app accesses . The portal tells the organization this at the permissions of the app: “They control who in your team may operate the app.” What the app accesses is shown separately under Data & access ."
  },
  {
    "id": "apps-sdk/data-access#dataaccess",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "dataAccess",
    "anchor": "dataaccess",
    "text": "If the block is missing, the most restrictive reading applies: the app reads nothing, fetches nothing, and keeps its own data on uninstalling. It is therefore needed only where there is something to say. Field Format Meaning Label in the portal reads List of tenant-profile , tenant-branding , media-library , team , player-location , fleet-locations , location-hours , default empty Existing data sets of the organization that the app reads. Reads existing data — per data set Organization details , Organization logo , Media library , Team and accounts , Display locations , All locations with address and position , Opening hours of the locations with public holidays writes List, max. 8 entries, each entry <dataset>:create-<thing> , ≤ 60 characters, default empty Write capabilities on data sets of other parties. Changing and deleting do not exist, and an app can only contribute. Adds data — “Changing or deleting is impossible. An app can only contribute.” external List, max. 8 entries with name (1–60), purpose (1–200), and optionally sends (≤ 200) External services the app reaches: whom, for what, and what goes out in the process. External connections — “What is sent: …” or “No data of your organization is sent.” retention kept or deleted , default kept What happens to the app’s own data on uninstalling. When you uninstall — with the names of the resources : “Locations and schedules are kept. You can have them deleted for good afterwards.” or “Uninstalling deletes ….”, and without"
  },
  {
    "id": "apps-sdk/data-access#the-location-of-the-display-player-location",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "The location of the display: player-location",
    "anchor": "the-location-of-the-display-player-location",
    "text": "A scene often runs in all branches of an organization. With player-location , a widget shows each display’s own place, such as the weather outside the door or the local time, without anyone maintaining a scene per branch. The location is whatever the organization has stored as the location for the display in the portal. It does not come through APP_TENANT_DATA but in the widget resolver’s context as ctx.location , because only there is it known which display is being resolved for: Field Content id Identifier of the location ( loc_… ), for example to link it with the app’s data name Name of the location as the organization maintains it (“Altona branch”) city , countryCode Place and country code (ISO 3166-1, DE ) of the address, each null without an address. Street and postal code are never passed. latitude , longitude Coordinates of the location, together null without coordinates timeZone Time zone of the location ( Europe/Berlin ) hours only with location-hours , see Opening hours The coordinates come from geocoding the address and are accurate to the building. Anyone who sends them to an external service rounds them to what the purpose needs and says so in sends and in the terms: the weather app sends two decimal places, about a kilometer, the same cell as its cache key. A resolver has to distinguish three states: ctx.location When absent The app does not name player-location under reads , or the organization has not yet consented to the area with the installed version. Only"
  },
  {
    "id": "apps-sdk/data-access#opening-hours-location-hours",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "Opening hours: location-hours",
    "anchor": "opening-hours-location-hours",
    "text": "Opening hours are master data of the location. The organization maintains them in the portal at the location (tab Opening hours ), through PATCH /v1/locations/{id} , or with the MCP tool set_location_opening_hours , and every app that shows them reads the same ones. The area is separate from player-location because opening hours are not a place specification: an app that needs only the place does not get them, and a consent of its own stays understandable. With location-hours , every location that the app reads ( ctx.location and location() , and with fleet-locations also locations() ) additionally carries hours : Field Content region Federal state per ISO 3166-2 ( DE-HH ) or null . Without a federal state, there are no public holidays. weekly Weekly hours, for each weekday ( mon … sun ) the windows { opens, closes } . If closes is not after opens , the window ends the next day, and identical times mean 24 hours. specialDays the special days that touch the period, { startDate, endDate, label, closed, windows } holidays the statutory public holidays in the period, { date, id, name } . id is stable ( easter-monday ) so that the display software can name the holiday in its language from , until the period: 21 days from today in the location’s time zone hours is absent without the area or without consent, and null means the app may read, but no hours are maintained at the location. What applies on a given day is computed by openingDayOf from @szenera/contracts/fleet with the prec"
  },
  {
    "id": "apps-sdk/data-access#all-locations-fleet-locations",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "All locations: fleet-locations",
    "anchor": "all-locations-fleet-locations",
    "text": "A fetch that must know in advance for which places it retrieves data reads the directory of locations: APP_TENANT_DATA.locations(tenantId) returns all of the organization’s locations ordered by name, with the full address ( street , postalCode , city , countryCode ), coordinates, and time zone, and with location-hours including hours . A resolver may not reach outside, so a job needs the places in advance, the Pharmacies on duty app the postal codes, for example. Anyone who sends the address to an external service states in dataAccess.external exactly what goes out. In tests, createMockAppContainer({ locationsMock: [...] }) supplies the locations for location() and locations() ."
  },
  {
    "id": "apps-sdk/data-access#permissions",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "permissions",
    "anchor": "permissions",
    "text": "The installation enters every permission into the organization’s permission catalog. Two lists, the same shape: List Who holds it defaultRoles tenant The organization’s team Roles that receive the permission with the installation, e.g., ['OWNER', 'MANAGER', 'MEMBER'] platform Platform operations empty ( [] ) — no team member receives the permission Each entry is a quartet: Field Format Meaning key <ns>.<verb> or <ns>.<resource>.<verb> ( ^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$ ), e.g., pharmacies-on-duty.location.read The key that the backend checks through APP_PERMISSION_GUARD . The platform prefixes it with apps:<id>: . label Text, at least 1 character Label in the permission catalog and in the installation dialog. description Text, at least 1 character One sentence on what the permission allows. defaultRoles List of roles Starting assignment. The organization can change it in the team area. The helper definePermission(key) only checks the format of a key and returns it unchanged. The bundled apps write the quartets directly into the manifest. New permissions in a new version are not adopted silently by the platform: as soon as the version is published as stable , the installation moves to Update waiting , the app’s widgets cannot be inserted in the scene editor, and the app’s Update tab shows the added permissions with Accept and Decline . A call that uses the app beforehand fails with Grant.PendingReacceptance (HTTP 409), and a consent to a different manifest version fails w"
  },
  {
    "id": "apps-sdk/data-access#unlocks-and-connects",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "unlocks and connects",
    "anchor": "unlocks-and-connects",
    "text": "An app does not have to bring a surface. unlocks and connects describe the second purpose of an app: not delivering something but unlocking something. The feature lives in the platform, and the app is the gate in front of it. Field Format Platform catalog Triggers consent? unlocks max. 8 keys, lowercase with hyphens, ≤ 40 characters video-walls ( Video walls ), library-places ( Places in the library ) No — the row gives the organization something, takes nothing from it, and reads nothing from it. connects max. 4 keys, lowercase with hyphens, ≤ 40 characters currently microsoft-graph The organization makes the connection itself in the portal. Acquiring, renewing, and revoking the authorization are handled once, centrally, in the platform. The app sees neither the authorization nor the access token. A key that is not in the catalog keeps the app from loading, because otherwise an app could promise a feature or connection with nothing behind it. Both fields are omitted rather than set empty: an empty field would shift the canonical hash of every existing version. The two gate apps show the patterns: video-wall declares unlocks: ['video-walls'] , and onedrive declares unlocks: ['library-places'] and connects: ['microsoft-graph'] . The store entry shows the unlocking in the Data & access tab under Unlocks in the portal . An app that only unlocks a core feature of the platform does not appear in the store as an app: video-wall appears there as the tile Core feature · Video wall ( c"
  },
  {
    "id": "apps-sdk/data-access#legal",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "legal",
    "anchor": "legal",
    "text": "Field Required Meaning disclaimerKey yes Key of the version that a consent refers to ( <group>.bedingungen.v1 ). A change to the substance of the notice bumps it ( v1 → v2 ). disclaimerSummary yes, max. 500 characters Short summary in plain text. The installation dialog ( Important note ), store entry, and widget footer show it unchanged. It presents the version, and rewording it under the same key does not require consent. termsPath no Path of the full text on the public website, ≤ 200 characters, lowercase letters, digits, hyphens, and slashes only ( /app-bedingungen/<group> ). The portal prepends its base and links it as The app’s full terms . Without termsPath , the dialog shows only the short summary. requireAcceptanceAt default install install : consent at installation and afterward with every new version of the notice. upgrade : consent with every new version of the app, even if the notice is unchanged. never : no consent. The bundled apps carry install . Apps with the same legal topic point to the same sheet. The bundled apps use seven groups: Group ( termsPath ) Apps /app-bedingungen/fremdinhalte webpage , youtube , stream , embed /app-bedingungen/eigene-inhalte notice-board , ticker , qr-code , menu-board /app-bedingungen/wetter weather /app-bedingungen/nachrichten rss , tagesschau , bbc , bild , ndr , spiegel , deutschlandfunk /app-bedingungen/verbindungen calendar , onedrive /app-bedingungen/notdienst pharmacies-on-duty /app-bedingungen/oeffnungszeiten opening-hou"
  },
  {
    "id": "apps-sdk/data-access#query-the-state-of-an-installation",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "Query the state of an installation",
    "anchor": "query-the-state-of-an-installation",
    "text": "An organization reads what has become of the manifest, permissions, and terms through two routes for its own installations. Both require a credential of the organization but no permission of their own : a valid sign-in suffices, and an API key reaches them regardless of which permissions are in its scope . An operator credential does not reach them, because both always answer about exactly one organization and reject a credential without an organization with 403 Permission.Denied . Route What the response contains GET /v1/me/apps Under apps , one row per installation: grantId (identifier tag_… ), appId , installedVersion , installedAt , plus the presentation fields of the installed version ( displayName , tagline , iconUrl , coverUrl , category , tier , bundleSizeBytes ), the data areas as resources , and sceneCount , in how many scenes the app is actually used. Three states sit side by side: status ( active or pending-reacceptance ), platformStatus ( active , beta , disabled ), and effect with status ( active , locked ) and reason ( ok , pro-required , premium-required , booking-required , exempt ). Uninstalled apps are missing from the list, while switched-off apps and apps no longer included in the package remain in it, because they are part of the installed base. Name, tagline, and data areas appear in the language from locale or Accept-Language , where the version carries them. GET /v1/me/apps/{appId}/installed The page of the installed app: entry describes the installed"
  },
  {
    "id": "apps-sdk/data-access#what-the-organization-sees",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "What the organization sees",
    "anchor": "what-the-organization-sees",
    "text": "The installation dialog carries the subtitle “Review what the app does with your data, then confirm the installation.” It shows the Data & access block, below it lists the permissions as Roles & permissions with their count, shows the short summary of the terms under Important note , links the full text as The app’s full terms , and requires the checkbox I have read the permissions and the app’s terms and agree to them. before Install takes effect. The same information is already in the app’s entry in the store, in the Data & access tab. Whoever is weighing whether they want the app at all should be able to read its terms before the click. The tab names which areas of its own the app creates (from resources ), which data sets it reads, what it adds, which connections it keeps to the outside and what is transferred in the process, and what happens to its data on uninstalling. Below that come Privacy with the recipients from dataAccess.external and the short summary of the terms with the link to the full text, which the portal sets from legal.termsPath . What the manifest leaves out, the portal states explicitly (“This app does not access any of your existing data.”) instead of omitting the card. GET /v1/me/apps/store/{appId} delivers the entry as StoreEntryView , fully decided: the app’s situation for the organization, exactly one action, and the reason with its numbers. The portal forms the sentences for it, and calculation happens only on the server. The handbook describes t"
  },
  {
    "id": "apps-sdk/data-access#related",
    "path": "/apps-sdk/data-access",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "data-access",
    "pageTitle": "Data access, permissions, and terms",
    "section": "Related",
    "anchor": "related",
    "text": "The manifest — the four blocks in the context of all fields. Configure widgets — what the widget catalog reveals about a widget’s configuration, and how a client checks it. What is an app, and how does it get onto a display? — the states of an installation in the lifecycle. Apps in the handbook — the organization’s view."
  },
  {
    "id": "apps-sdk/signing-and-catalog",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "Signing and the path into the catalog",
    "anchor": "",
    "text": "A release freezes the version, manifest, and bytes of an app and moves through the channels up to stable . This page covers what belongs to a release, how a version enters the catalog as a signed artifact, the rules of the channels, and the calls and commands with which platform operations manages the catalog. Each version comes into being exactly once: CI builds it from its tag as a signed, immutable artifact <appId>-<version>.szapp with the manifest, the bundles for the display software and the editor, images and texts for the store, and the release notes, and every environment fetches this one artifact from the registry. The same version therefore has the same bytes in every environment."
  },
  {
    "id": "apps-sdk/signing-and-catalog#what-belongs-to-a-release",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "What belongs to a release",
    "anchor": "what-belongs-to-a-release",
    "text": "In object storage, a version lives under platform/apps/<appId>/<version>/ : Location Content bundle.zip The artifact unchanged. Its sha256 is the digest of its layer in the registry. manifest.json The version’s manifest in canonical JSON — the catalog stores it embedded, and its hash is the manifestHash . runtime/ The unpacked files of the runtime bundle that the player loads. authoring/ The unpacked authoring bundle with the configuration form for the scene editor, if the app has one. store/ Icon and images for the store. The release additionally carries sizeBytes and sha256 (64 lowercase hexadecimal characters) of the stored bytes and the provenance: source ( registry for an artifact from the registry, seed-placeholder for a row without bytes that the seed created without access to the registry), under artifact the hash of the index, the release key whose signature has been verified, the signature itself, and the digest in the registry, plus importedAt and importedById . Manifest and bytes are immutable after fetching, and the only fields that can be changed afterward are the notes. They live under notes per audience, namely customer (“New in this version”), operator , and developer , and per language ( de-DE , en-US , …), as the artifact’s index brings them. In addition, every release still has the one older note, releaseNotes (max. 2000 characters): the text for operations in de-DE , otherwise the one for customers. Devices and clients reach a version through four routes."
  },
  {
    "id": "apps-sdk/signing-and-catalog#the-two-catalogs-across-all-apps",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "The two catalogs across all apps",
    "anchor": "the-two-catalogs-across-all-apps",
    "text": "Besides the individual version, the API delivers two overviews of the whole catalog. Both take, for each app, the version that applies in the stable channel, the one that entered it most recently and not the one with the highest number, and break down its manifest: one into widget definitions, the other into resource types. The widget catalog additionally lists the six built-in building blocks (text, image, video, shape, graphic, document). They belong to no app and therefore have the reserved identifier core . What exists only in canary or beta appears in neither, and both answer regardless of what an organization has installed: they describe the offering, not the installed base. Route What the response contains GET /v1/widget-catalog Under entries , one entry per widget with appId , version , and the complete definition : id , name , description , the configSchema as JSON Schema with field texts and examples (see Configure widgets ), size , noResolver , and the preview block with defaultConfig , mockData , thumbnailUrl , variants , and states . The two gate apps without a widget contribute nothing, and the six built-in building blocks are included under appId: core . GET /v1/resource-type-catalog Under entries , one entry per resource type with appId , version , and the definition : id , slug , name , description , listEndpoint , authoringPath , and the three schemas baseSchema , createSchema , and updateSchema . Only apps with resources in the manifest contribute — among t"
  },
  {
    "id": "apps-sdk/signing-and-catalog#signature-the-statement-about-the-version",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "Signature: the statement about the version",
    "anchor": "signature-the-statement-about-the-version",
    "text": "Signing happens in CI, never on a server: CI signs the statement “this app, this version, this manifest, this index” with the Ed25519 release key of each world ( app-release-staging for development and staging, app-release-prod for production). When fetching, the API checks the signature of its own world against that world’s public key. Without a valid signature of its world, no version enters the catalog, and there is no fallback to the sha256 alone. Whoever fetches a release signs nothing. On the device, remote maintenance checks every artifact before it unpacks anything: the sha256 value of the downloaded archive must match the one on the release, then it checks the signed statement against the anchor app-release-<world> in the image, after that the manifest and every file against the index, and it unpacks only runtime/ , safeguarded against paths that lead out of the target folder. If the anchor is missing, it rejects every artifact. An archive without an index is not an artifact, and remote maintenance does not accept it. The signing entry in the manifest changes nothing about this and remains only because it appears in the manifests of published versions."
  },
  {
    "id": "apps-sdk/signing-and-catalog#fetch-a-release",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "Fetch a release",
    "anchor": "fetch-a-release",
    "text": "A release comes into being by importing an artifact from the registry. CI reports every version it has stored, to staging and production. The API does not list the versions of the registry: whoever fetches a version by hand names its version themselves. POST /v1/platform/app-catalog/imports with the body { \"appId\": \"…\", \"version\": \"…\" } fetches the version. Optionally, digest names the digest of the OCI manifest ( sha256:… ) that the caller expects, and CI always sends it. The API checks manifest and layer in the registry, then the archive: every file against its checksum in the index, the manifest against the schema and its hash, identifier and version, and the signature of its world. Only then does it store the bytes and create the release in the canary channel. The response is { \"release\": …, \"created\": true } with 201 if the version is new in the catalog. If it was already there with the same artifact, the call answers with 200 and created: false , so the call can be repeated. The import fills a placeholder without bytes that has the same manifest, while the row’s channel and the releases already made into channels remain. It replaces the seed’s note, which only describes the placeholder, with the notes from the index, unless someone has changed it. In doing so, the import backfills the notes: if a version that it already finds in the catalog lacks an audience or language that its artifact’s index carries, it adds it and records that as notes-updated with its actor. That "
  },
  {
    "id": "apps-sdk/signing-and-catalog#the-channels",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "The channels",
    "anchor": "the-channels",
    "text": "A new release always starts in canary , and nobody chooses the starting channel. Each promotion moves exactly one step, and a jump is not possible. No single person promotes to stable : one person requests, a second confirms, also when restoring. The version that entered stable most recently is the one that applies there, not the one with the highest number. Channel Label in the operator portal Meaning canary Canary Freshly fetched — first trial channel. beta Beta Extended trial. stable Stable The version recommended for organizations. deprecated Deprecated Demoted but still usable. retired Retired Out of service. End state with a retirement period. Call Body Rule POST …/releases/{id}/promote { \"toChannel\": \"beta\" } , to stable with \"reason\": \"…\" canary → beta always promotes immediately. To stable , the call promotes immediately if the platform does not require a second person ( releaseApproval.apps set to off , the default). If it requires one ( stable ), the call with reason (10–500 characters) creates a request, response { \"approval\": … } instead of { \"release\": … } , and without reason it answers AppRelease.SecondPersonRequired (HTTP 409). At most one request waits per version and operation ( ReleaseApproval.AlreadyOpen , HTTP 409), and it is valid for seven days. Everything else: Channel.InvalidTransition (HTTP 409). POST …/releases/{id}/restore { \"reason\": \"…\" } , required (10–500 characters) Returns a version from deprecated , or a superseded one in stable , to stable"
  },
  {
    "id": "apps-sdk/signing-and-catalog#the-history-of-an-app",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "The history of an app",
    "anchor": "the-history-of-an-app",
    "text": "Every movement of a release is recorded as an entry in the history of its app: creation, fetching from the registry, every release into a channel, demoting, restoring, retiring, retirement periods, changed notes, and every step of a request to a second person ( promotion-requested , promotion-approved , promotion-withdrawn , promotion-expired ). If a second person confirmed, that person is the actor, and requestedById names the person who requested. Each entry names the channel from and to, the time, and the actor: a person in operations ( operator with identifier), CI ( ci ), an environment’s seed ( seed ), the platform itself ( auto ), the zero hour ( zero-hour ), or legacy for what happened before the history and is reconstructed from the releases’ timestamps. Call For whom What it delivers GET /v1/platform/app-catalog/apps/{appId}/history Operations ( platform.app-catalog.list ) every entry, newest first, in pages with limit and cursor GET /v1/apps/{appId}/history?locale=en-US every signed-in organization the versions that have reached stable , newest first: identifier ( releaseId , the same as on installation, proof of play, and update decision), number, since when it has been open to everyone ( stableSince ), “New in this version” in the requested language or, failing that, in German ( note , null without text), and whether it is retired. For an app that operations has switched off for the organization, the call answers with App.NotFound (HTTP 404)."
  },
  {
    "id": "apps-sdk/signing-and-catalog#as-cli-commands",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "As CLI commands",
    "anchor": "as-cli-commands",
    "text": "The CLI maps the lifecycle after fetching in nine commands. It does not know fetching itself: the environment syncs with the registry every ten minutes, and CI speeds it up with the call above. The requests to a second person are run by the group approval ( list , approve , withdraw ). Command Purpose app-release list The releases of the app catalog, filterable with --app and --channel . app-release set-notes Change release notes. app-release promote Promote to beta or stable with --channel . To stable, the command promotes immediately or, if the platform requires it, creates a request to a second person, and --reason is then required. app-release restore Restore a deprecated or superseded version to stable, --reason is required. Immediately or as a request, depending on the setting. app-release demote The way back when a stable release turned out not to be stable after all: --channel=deprecated , the only permitted step, from stable . app-release retire Take a release out of service. --reason and --grace=regular|security|immediate are required, and --advisory carries the security note and belongs exactly to --grace=security . app-release grace-period Extend the retirement period ( --months or --until=<yyyy-mm-dd> ) or end it immediately ( --immediately with --reason ), exactly one of these. app-release delete-artifact Delete the bytes of a version, --yes is required. app-release cleanup Clean up the bytes of superseded versions of an app, --dry-run or --yes , plus --keep-new"
  },
  {
    "id": "apps-sdk/signing-and-catalog#what-the-organization-sees-from-this",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "What the organization sees from this",
    "anchor": "what-the-organization-sees-from-this",
    "text": "New stable version without new permissions and without changed terms: Existing installations adopt it automatically, and the earlier decisions in the app’s Update tab show the entry adopted silently . New stable version with new permissions or changed terms: The installation moves to Update waiting , and the app’s widgets cannot be inserted in the scene editor until the organization accepts in the Update tab (see Data access, permissions, and terms ). Publishing a scene: If a widget pins a version that does not exist, that is retired and whose retirement period is over, or if it belongs to an app that the organization has not installed, the platform rejects publishing and states the reason in one sentence. A retired version within an ongoing period is a notice. Retired release: No longer selectable for new installations. Whoever has installed exactly this version gets the successor, the lowest stable version above it. If it requires nothing new and the organization has not declined it, the platform adopts it immediately. Otherwise the installation moves to Update waiting , with the end of the retirement period as the deadline, and after that the platform switches. Without a successor, the app keeps running unchanged until the end of the period."
  },
  {
    "id": "apps-sdk/signing-and-catalog#what-does-not-exist-today",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "What does not exist today",
    "anchor": "what-does-not-exist-today",
    "text": "The catalog is purely curated: CI builds and signs every version, and platform operations fetches it into the catalog and steers its lifecycle. There is no developer submission, no review queue, and no statement about review times or criteria. The 26 bundled apps are created in the platform’s source tree, built from their tag, signed as an artifact, and fetched into every environment. Anyone preparing an app does so with platform operations, and the pages of this area describe the contract it is written against."
  },
  {
    "id": "apps-sdk/signing-and-catalog#related",
    "path": "/apps-sdk/signing-and-catalog",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "signing-and-catalog",
    "pageTitle": "Signing and the path into the catalog",
    "section": "Related",
    "anchor": "related",
    "text": "What is an app, and how does it get onto a display? — the lifecycle in a picture. The manifest — signing and the other fields. Commands for platform operations — app-release and app-status in the szenera-ops tool."
  },
  {
    "id": "apps-sdk/example-apps",
    "path": "/apps-sdk/example-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "example-apps",
    "pageTitle": "The example apps",
    "section": "The example apps",
    "anchor": "",
    "text": "The bundled apps are the real consumers of the contract described in this area. The first table sorts each app by profile, backend building blocks, resources, and signature. The second lists category, class, and the group of terms. Three apps are the references that a new app takes its cue from."
  },
  {
    "id": "apps-sdk/example-apps#three-profiles-three-references",
    "path": "/apps-sdk/example-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "example-apps",
    "pageTitle": "The example apps",
    "section": "Three profiles, three references",
    "anchor": "three-profiles-three-references",
    "text": "Profile What is added Reference Pages Widget only A widget with noResolver: true , runtime and authoring bundles, no backend, no resources, no app database. clock — the smallest complete app: one manifest, one widget, two bundles. Widget, configuration, and preview Plus fetcher A backend with @WidgetResolver and @Fetcher against an external source, no app database. weather — the city lives in the widget configuration, and the fetcher loads current conditions and forecast into the cache. Backend: resolvers, fetchers, and jobs Plus resources and database Resource types that the organization maintains in the portal, a schema of its own apps_<id> , and a @JobHandler if needed. pharmacies-on-duty — uses every building block of the SDK: resolver, fetcher, a job every 30 minutes, two resources, legal , signing: 'required' . The manifest , Data access, permissions, and terms The levels are not strictly cumulative: menu-board has resources and a schema of its own but fetches nothing from the network. “External source” and “own data” are two independent building blocks. pharmacies-on-duty is the one app that has both. A new app clones the structure of the most similar reference."
  },
  {
    "id": "apps-sdk/example-apps#all-apps",
    "path": "/apps-sdk/example-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "example-apps",
    "pageTitle": "The example apps",
    "section": "All apps",
    "anchor": "all-apps",
    "text": "The five brand apps bbc , bild , ndr , spiegel , and deutschlandfunk inherit the widget, backend, and presentation from tagesschau . They carry only a manifest of their own and their identity (identifier, label, accent color, theme, font). The gate apps onedrive and video-wall bring no widget. App Widget Profile Backend building blocks Resources / database Signature Special feature clock (“Clock”) clock widget only — — optional Reference for the “widget only” profile. The time zone can be overridden per instance. webpage (“Web page”) iframe widget only — — optional The only app that retrieves an external address without a backend: the player loads the page itself. notice-board (“Notice board”) rich-text widget only — — optional Safe authoring markup: a Markdown subset becomes a typed block model, never HTML text. countdown (“Countdown”) countdown widget only — — optional Counts down to a target time. The target, units, and expired text are set in the configuration. countup (“Day Counter”) countup widget only — — optional Counts up since an event and highlights milestones. world-clock (“World Clock”) world-clock widget only — — optional Several time zones side by side as a list of equal-ranking tiles. word-clock (“Word Clock”) word-clock widget only — — optional Time as a sentence instead of a number, grid display with language selection. ticker (“Scrolling Text”) ticker widget only — — optional The only app with continuous motion. On frozen surfaces it shows a still image of "
  },
  {
    "id": "apps-sdk/example-apps#category-class-and-terms",
    "path": "/apps-sdk/example-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "example-apps",
    "pageTitle": "The example apps",
    "section": "Category, class, and terms",
    "anchor": "category-class-and-terms",
    "text": "category is the platform’s single ordering axis: it determines the filter in the store and the section of the apps in the scene editor’s palette. tier is the class: standard carries no badge in the store, pro carries the Pro badge, and addon carries the Add-on badge and is booked as an add-on. How an app draws its content within its frame is decided by a configuration field, for example four layout values for pharmacies-on-duty and three composition values for clock . App category tier legal group clock zeit standard — countdown zeit standard — countup zeit standard — world-clock zeit standard — word-clock zeit standard — weather information standard wetter rss information pro nachrichten tagesschau information pro nachrichten bbc , bild , ndr , spiegel , deutschlandfunk information pro nachrichten calendar information pro verbindungen webpage web-video standard fremdinhalte youtube web-video standard fremdinhalte stream web-video standard fremdinhalte embed web-video standard fremdinhalte notice-board gestaltung pro eigene-inhalte ticker gestaltung standard eigene-inhalte qr-code gestaltung standard eigene-inhalte pharmacies-on-duty geschaeft pro notdienst opening-hours geschaeft pro oeffnungszeiten menu-board geschaeft pro eigene-inhalte onedrive geschaeft standard verbindungen video-wall geschaeft addon — Apps without a tier in the manifest carry the default standard . What the preview variants of an app do in the store is covered under Widget, configuration, and preview ."
  },
  {
    "id": "apps-sdk/example-apps#what-is-not-covered-here",
    "path": "/apps-sdk/example-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "example-apps",
    "pageTitle": "The example apps",
    "section": "What is not covered here",
    "anchor": "what-is-not-covered-here",
    "text": "Text, image, video, shape, graphic, and document are not apps but core widgets: they have the reserved identifier core , do not appear in the store, need no installation, and go through neither release nor signing. Anyone who builds a widget that gets by without a data source, without permissions, and without a product promise of its own is probably building a basic building block and not an app. The user’s view of each app (what it shows, how it is set up, which terms it brings) is in the handbook: Apps in the handbook ."
  },
  {
    "id": "apps-sdk/example-apps#related",
    "path": "/apps-sdk/example-apps",
    "area": "apps-sdk",
    "areaTitle": "Apps SDK",
    "page": "example-apps",
    "pageTitle": "The example apps",
    "section": "Related",
    "anchor": "related",
    "text": "Widget, configuration, and preview — formats, preview variants, and preview states using the clock example. Backend: resolvers, fetchers, and jobs — the building blocks of the fetcher and data apps. Apps in the handbook — the organization’s view of each app."
  },
  {
    "id": "changes/index",
    "path": "/changes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "index",
    "pageTitle": "Changes",
    "section": "Changes",
    "anchor": "",
    "text": "Where changes to the API come from, what a breaking change is, and how you compare the specification against your version, without promising anything that does not exist."
  },
  {
    "id": "changes/index#keep-track",
    "path": "/changes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "index",
    "pageTitle": "Changes",
    "section": "Keep track",
    "anchor": "keep-track",
    "text": "Page When you need it Stability classes and sunset What stable, beta, and internal promise, how long the period between announcement and shutdown is, and how your client recognizes a sunset on its own. Release trains, notes, and deprecations How a release train comes about, how its notes are structured, what is attached to the release, and what applies to deprecations today. Packages and their versions Which version of the API client, Apps SDK, CLI, and contracts applies and what changed for developers in each version. Compare the specification against your version Save openapi.json with a date, reload it with every train, and compare paths, required fields, and error codes, with the site and your own environment side by side."
  },
  {
    "id": "changes/stability-and-sunset",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "Stability classes and sunset",
    "anchor": "",
    "text": "Every operation of the specification carries a class, and an operation that is being sunset announces itself in its own responses. This page says what stable , beta , and internal promise, how long the period between announcement and shutdown is, and how a client recognizes the sunset without anyone reading a page."
  },
  {
    "id": "changes/stability-and-sunset#the-three-classes",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "The three classes",
    "anchor": "the-three-classes",
    "text": "Every operation carries the extension x-stability : Class What it promises stable Path, required fields, and error codes do not change without a new version. A field can be added, none disappears. Until beta there is one exception, see below . beta Public, but young: the shape can still move until it has survived a cycle without a breaking change. Build on it, but read the notes. internal Not part of the public surface. These operations are neither in the reference nor in the specification on this page. The class is set, not guessed: an operation without a class aborts the generation of the public version. An unclassified route therefore never appears publicly by accident, and a public one does not silently disappear either. What internal carries is the inside view of the house: the operator console, the bridges to the device and the display software, the inbound endpoint of the payment provider, and diagnostics. There is nothing for an organization to call there. A call ends with 403 Permission.OperatorOnly or does not reach you at all."
  },
  {
    "id": "changes/stability-and-sunset#how-a-sunset-works",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "How a sunset works",
    "anchor": "how-a-sunset-works",
    "text": "Twelve months pass between the announcement and the shutdown of an operation, from beta on without exception ( until then ). In the specification. The operation carries deprecated: true and x-sunset with the shutdown date. If you generate a client from the specification, you see the sunset at build time, and many generators then mark the method as deprecated. In every response. The operation keeps responding but carries three additional headers: On the deadline. After the date from Sunset , the operation no longer responds. So even someone who has never read the specification sees the sunset: in the log of their own client."
  },
  {
    "id": "changes/stability-and-sunset#until-beta-exceptions-to-the-promise",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "Until beta: exceptions to the promise",
    "anchor": "until-beta-exceptions-to-the-promise",
    "text": "The platform is in alpha until the end of 2026. Until beta, even routes of the stability class stable may be dropped or broken without a sunset. Every break is listed in the change notes of the train, under “Developers”, with the route and what takes its place. From beta on, the promises above apply without exception. The train with the new analytics made use of this: Route What happened GET /v1/analytics/playback , GET /v1/players/{playerId}/playback , GET /v1/media/assets/{assetId}/playback dropped without a sunset. The new analytics replaces them: GET /v1/analytics/overview , GET /v1/analytics/players/{playerId}/day and /plays , GET /v1/analytics/content , and POST /v1/analytics/explore GET /v1/analytics/insights/overview , POST /v1/analytics/insights/report dropped without a sunset, replaced by the overview and the ad hoc analysis /v1/analytics/reports and the routes below it changed shape: The query of a report is now that of the ad hoc analysis instead of a metric of the old measurement series, and formats exist only as csv and pdf . beta since then A client generated from the specification, and @szenera/client from before this train, get 404 for the dropped routes and 400 for the old report forms. What changes with each train is described under Release trains, notes, and deprecations ."
  },
  {
    "id": "changes/stability-and-sunset#what-your-client-should-do-about-it",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "What your client should do about it",
    "anchor": "what-your-client-should-do-about-it",
    "text": "Rule Why Watch for Deprecation in every response and log the finding once per route A sunset rarely hits all calls at the same time. One line per route shows exactly what you still use. Record Sunset and turn it into a deadline Twelve months are long enough to forget and short enough that it is then too late. Do not branch on Link The address leads to the reference. It is a reading aid for people, not a redirect target. For beta , compare the specification regularly See Compare the specification against your version . Two guards keep the policy honest: an announcement without x-sunset is an error in the build, and so is an elapsed x-sunset on a route that still responds."
  },
  {
    "id": "changes/stability-and-sunset#where-to-read-the-current-state",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "Where to read the current state",
    "anchor": "where-to-read-the-current-state",
    "text": "Source What it says https://developer.szenera.com/api/openapi.json the public specification as of the current train, with x-stability and x-sunset per operation GET /v1/openapi.json on your environment the same information for the version that actually runs there What’s new in the handbook what changed in the interface with each train, newest first The status of the platform the current state, announced maintenance windows, and the latest incidents The class also appears in the reference on every operation, see Read the reference ."
  },
  {
    "id": "changes/stability-and-sunset#related",
    "path": "/changes/stability-and-sunset",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "stability-and-sunset",
    "pageTitle": "Stability classes and sunset",
    "section": "Related",
    "anchor": "related",
    "text": "Base URL, /v1, and stability Compare the specification against your version Release trains, notes, and deprecations"
  },
  {
    "id": "changes/release-notes",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "Release trains, notes, and deprecations",
    "anchor": "",
    "text": "The API changes in trains: a train bundles the versions of the services on one date, and each train gets a note that lists breaking changes before anything else. This page says how both are structured, where the notes are today, and what applies to deprecations."
  },
  {
    "id": "changes/release-notes#the-release-train",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "The release train",
    "anchor": "the-release-train",
    "text": "A train is a cut through time, not a version contract. It therefore carries no major, minor, or patch of its own but its date. Attribute What applies Name release/YYYY-MM-DD , with several trains on the same day release/YYYY-MM-DD.2 (a .1 never exists) What it carries the services of the platform: the API, the portal, Szenera Ops, the home page, and the display software of the displays Versions each service carries its own number (such as 0.4.14 ). The train lists only those services as moved whose number has risen since the last train. The others appear as unchanged in the note When a train is due as soon as the version of one of these services is above that of the last train. In practice, this happens when the collected change notes become new versions What the train ships the same images that ran on staging before. They are not rebuilt, only labeled with their version What does not travel in the train the operating system of the players. It follows its own cycle and its own triggers, and so does the display extension for the browser. Libraries such as the API client and the Apps SDK, and the apps, travel along in the services. The note lists their versions under “Packages” GET /health/version returns the version of the service that responds in your environment, without signing in, with the fields service , version , builtAt , and environment . In production, version holds only the number ( 0.4.1 ). On all other environments, the short commit hash of the build is appended ("
  },
  {
    "id": "changes/release-notes#how-a-release-note-is-structured",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "How a release note is structured",
    "anchor": "how-a-release-note-is-structured",
    "text": "The text of a release note comes from the change notes that every change to the platform brings along. Whoever writes a change adds a note for it and writes lines in it per package, each with a kind and an audience: for customers, for operations, or for developers. The release note of a train is addressed to operations. It names the breaking changes and the steps of deployment, and for customers and developers it refers to their pages. It does not claim what nobody wrote down. Part Content Title Szenera <date> Breaking changes for each breaking change for developers and operations, one line with the package, the upgrade step, and a link to the change that brought it Deployment the steps for operations, numbered continuously: first before deploying, then while deploying, then after For customers the link to the changes page of the handbook and the number of apps that explain their new features in the store For developers the link to the change list per package and to the package page of the developer site Packages libraries, apps, and extension whose version rises with the train, with the version before Delivery a table Service · Version · Before · Digest over all services of the train Comparison a link to all changes since the last train A train without a single change note says so in one sentence and lists only the versions."
  },
  {
    "id": "changes/release-notes#what-a-breaking-change-is",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "What a breaking change is",
    "anchor": "what-a-breaking-change-is",
    "text": "A breaking change is a change that forces you to act: a path that disappears or moves, a new required field, a different error code. It is marked in the note itself: the line carries breaking and names the upgrade step, and the level of the package rises. As long as the first digit of a version is 0 , the second digit is the boundary for breaking changes: 0.4.21 to 0.5.0 is a breaking change, 0.4.21 to 0.4.22 is not. What to do appears as the upgrade step, word for word as the author wrote it. A shortened instruction would be a wrong one. A breaking change appears at the very top, in the Breaking changes section. For the packages, the change list also shows it: there it appears under Breaking changes , see Packages and their versions ."
  },
  {
    "id": "changes/release-notes#what-is-attached-to-the-release",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "What is attached to the release",
    "anchor": "what-is-attached-to-the-release",
    "text": "Attachment Content manifest.json the train in machine-readable form: services, versions, digests, risen packages, consumed change notes openapi.json the specification as of this train two SBOMs (CycloneDX) the bills of materials of the dependencies from the same state SHA256SUMS checksums over all attachments A missing attachment does not hold up the train. The bills of materials are created in a separate run that can be slower than the train. If something is missing, the note says so in one line: “Not attached: … — could not be obtained in the run”."
  },
  {
    "id": "changes/release-notes#where-the-notes-are-today",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "Where the notes are today",
    "anchor": "where-the-notes-are-today",
    "text": "The notes are created as a release entry in the source code archive of the platform. This archive is not public, and this page lists no entries from it. Five things are reliably reachable without signing in: Source What it says https://developer.szenera.com/api/openapi.json the specification as of the current train GET /v1/openapi.json of your environment the specification of the version that actually runs there. The response may be cached for five minutes ( Cache-Control: public, max-age=300 ) Packages and their versions version and change list of the API client, Apps SDK, CLI, and contracts, section for developers What’s new in the handbook what changed in the interface with each train, newest first, without signing in The status of the platform the current state, announced maintenance windows, and the latest incidents Both versions of the specification can differ if your environment runs an older train. A comparison of the two files is therefore the way to recognize a change reliably."
  },
  {
    "id": "changes/release-notes#what-applies-to-deprecations",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "What applies to deprecations",
    "anchor": "what-applies-to-deprecations",
    "text": "Question Status today How is an operation marked? Every operation carries x-stability with stable , beta , or internal . Today, 372 of the 443 public operations are stable . What does stable mean? Path, required fields, and error codes do not change without a new version. A field can be added, none disappears. Are there deprecated operations? None today. If one were added, it would carry deprecated: true and x-sunset in the description as well as Deprecation , Sunset , and Link in every response. Is there a procedure with deadlines? Yes: twelve months pass between announcement and shutdown. The details are under Stability classes and sunset . How do I recognize a change that affects me? It appears in the note under Breaking changes , at the very top, with the required action verbatim, and it shows in the comparison of the specifications. What about routes I cannot find in the reference? Routes marked internal are not part of the public surface. They appear in no file on this page and in no promise."
  },
  {
    "id": "changes/release-notes#related",
    "path": "/changes/release-notes",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "release-notes",
    "pageTitle": "Release trains, notes, and deprecations",
    "section": "Related",
    "anchor": "related",
    "text": "Stability classes and sunset Compare the specification against your version Base URL, /v1, and stability"
  },
  {
    "id": "changes/packages",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "Packages and their versions",
    "anchor": "",
    "text": "Version and changes of the packages for developers — API client, Apps SDK, CLI, and contracts, newest first. The list is built with every new version from the change notes of the platform and shows their section for developers. There is no public way to obtain the packages today; the page says which version applies and what changed in it. Package Version @szenera/cli 0.4.0 @szenera/client 0.6.0"
  },
  {
    "id": "changes/packages#szeneracli",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "@szenera/cli",
    "anchor": "szeneracli",
    "text": ""
  },
  {
    "id": "changes/packages#040--2026-10-25",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.4.0 · 2026-10-25",
    "anchor": "040--2026-10-25",
    "text": "Breaking changes The stored sign-in state carries the fields token and expiresAt . Upgrade: An old sign-in state counts as signed out, and the next sign-in writes the new one. ( #821 ) Added New commands szenera proof list , szenera proof show , and szenera proof download . ( #822 ) New commands szenera analytics overview , szenera analytics explore , and szenera analytics delivery <acp_…> . ( #822 ) New commands advertiser list , advertiser get , advertiser create , campaign list , campaign get , booking list , creative list , and creative approve . ( #822 ) Changed The archive follows the client with the revenue statements, without a new command. ( #822 ) The archive follows the client without the read routes of the minute counting and the insights. The commands stay. ( #822 ) The archive follows the client with the panel sources cec , ddc , and none . The commands stay. ( #822 ) The command line ships the client that knows a sign-in host of its own. Without a setting, nothing changes. ( #825 ) The archive follows the client with campaignId per proof of the campaign report. The commands stay. ( #822 ) The archive bundles the current state of the contracts, and the commands of szenera stay the same. ( #847 ) szenera playlist get shows an ad break as \"Werbeblock · 3 Plätze à 10 s\" with its duration. ( #822 )"
  },
  {
    "id": "changes/packages#030--2026-10-23",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.3.0 · 2026-10-23",
    "anchor": "030--2026-10-23",
    "text": "Breaking changes Die JSON-Ausgabe der Kommandozeile nennt dieselben Namen wie die API. Umstieg: Skripte, die die JSON-Ausgabe lesen, auf die englischen Feldnamen umstellen. ( #761 ) (German only) szenera api-key revoke <akm_…> --compromised nennt den Grund des Widerrufs. szenera api-key list mit einem Schlüssel-Profil antwortet 401. Umstieg: Für szenera api-key list ein Profil mit Sitzung verwenden. ( #762 ) (German only) Added Die Typen folgen der API: Feed-Katalog, Quellen für den Betrieb und der Fehlercode FeedSource.NotFound . ( #801 ) (German only) Neue Aufrufe: Zusammenfassung, Sammelaktion und Wiederherstellen von Benachrichtigungen, dazu state und groupKey an der Liste. ( #795 ) (German only) playerId an der Vorschau der Widget-Daten und der Lesebereich player-location . ( #801 ) (German only) Standorte: region und openingHours , dazu das Ereignis fleet.location.opening-hours-changed . ( #801 ) (German only) Widget-Definitionen des Katalogs kennen size und preview.states , Ansichten des Stores kennen size . ( #801 ) (German only)"
  },
  {
    "id": "changes/packages#023",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.2.3",
    "anchor": "023",
    "text": "Changes GET /v1/audit/bulk/:correlationId entfällt und mit ihm client.audit.auditBulk und das Schema GetBulkActionDetailResponse . ( #742 ) (German only) Das Recht audit.bulk.read , die Ereignisart audit.bulk-action.completed und der Fehlercode Internal.AuditBulkActionDetailRepositoryUnavailable entfallen. ( #742 ) (German only)"
  },
  {
    "id": "changes/packages#022",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.2.2",
    "anchor": "022",
    "text": "Changes @szenera/contracts : neu CatalogAppSchema , AppReleaseNotesSchema (Notizen je Zielgruppe und Sprache), AppReleaseEventSchema , RestoreAppRelease* , ListAppReleaseHistory* und GetAppVersionHistory* . Ein AppRelease trägt notes , die Wire-Form daneben weiter releaseNotes . PATCH /v1/platform/app-catalog/apps/{appId}/releases/{id} nimmt notes je Zielgruppe oder wie bisher releaseNotes . ( #677 ) (German only) Ein Grant trägt installedReleaseId , pendingReleaseId , pendingReasons und pendingDeadline , ein Zustimmungs-Nachweis releaseId , eine offene Zustimmung fromVersion , toVersion , reasons und deadline . Entfallen sind autoUpgrade am Grant und ConfigSchemaChangedPayloadSchema . ( #677 ) (German only) GET /v1/apps/{appId}/history liefert einer Organisation die Fassungen einer App, die stable erreicht haben, mit Datum und „Neu in dieser Fassung\" in der erbetenen Sprache ( ?locale=en-US , ohne Angabe Deutsch). ( #677 ) (German only) @szenera/api-client : neu client.platform.appCatalogAppsHistory.list , client.platform.appCatalogAppsReleases.restore und client.appCatalog.appsHistory.list . @szenera/client : neu client.appCatalog.appsHistory.list . @szenera/cli bündelt die neuen Verträge, ihre Befehle bleiben, wie sie sind. ( #677 ) (German only) Das scenes-Bündel der Momentaufnahme trägt appVersions (je App die eine Fassung des Displays) sowie appRendererHosts und appWidgetTypes je <appId>@<version> , alle mit der Vorgabe {} . Die Pins der Szenen und Widgets nennen diesel"
  },
  {
    "id": "changes/packages#021",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.2.1",
    "anchor": "021",
    "text": "No entry for developers."
  },
  {
    "id": "changes/packages#020",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.2.0",
    "anchor": "020",
    "text": "Breaking changes Die Kommandozeile heißt jetzt szenera . Das Paket @szenera/cli liefert das Programm unter diesem Namen aus, der Name signage entfällt ohne Übergang. ( #618 ) (German only) Die Umgebungsvariablen heißen SZENERA_PROFILE , SZENERA_BASE_URL , SZENERA_API_KEY , SZENERA_OUTPUT und SZENERA_PROFILE_FILE . Variablen mit dem Präfix SIGNAGE_ liest die Kommandozeile nicht mehr. ( #618 ) (German only) Die Profil-Datei liegt unter ~/.config/szenera/profile.json . Ein zwischengespeichertes Zugriffsmerkmal liegt daneben, nach dem Umzug meldet sich die Kommandozeile einmal neu an. ( #618 ) (German only) In der Sitzungsliste erscheint die Kommandozeile als szenera-cli mit ihrer Fassung. ( #618 ) (German only) Die Befehle des Plattformbetriebs sind nicht mehr enthalten. szenera führt 122 Befehle für Organisationen: Medien, Szenen, Programme, Geräte, Team, Schlüssel, Abonnement und den Notausgang api . ( #618 ) (German only) Ein Profil für den Plattformbetrieb ( \"audience\": \"operator\" ) weist die Kommandozeile beim Lesen ab. ( #618 ) (German only) permission catalog --key-eligible zeigt jetzt die Rechte, die ein Schlüssel der eigenen Organisation tragen darf. ( #618 ) (German only) invoice list kennt die Flagge --status nicht mehr. Für eine Organisation blieb sie bisher ohne Wirkung. ( #618 ) (German only) Die Pakete heißen jetzt @szenera/* und die Apps @szenera-app/* , Importe und Abhängigkeiten sind entsprechend anzupassen. ( #610 ) (German only) Changes POST /v1/devices/:id/r"
  },
  {
    "id": "changes/packages#010",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.1.0",
    "anchor": "010",
    "text": "Stand vor den Änderungsnotizen — die erste Fassung mit eigener Zahl. Was davor geschah, steht im Verlauf des Quellbaums; diese Liste beginnt mit der nächsten Fassung. (German only)"
  },
  {
    "id": "changes/packages#szeneraclient",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "@szenera/client",
    "anchor": "szeneraclient",
    "text": ""
  },
  {
    "id": "changes/packages#060--2026-10-25",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.6.0 · 2026-10-25",
    "anchor": "060--2026-10-25",
    "text": "Breaking changes analytics.analyticsReports is @beta and carries a SavedExploreQuery as its query. The only formats are csv and pdf . Upgrade: Recreate reports as a query of the ad hoc analysis. ( #822 ) analytics.analyticsPlayback , analytics.playersPlayback , analytics.mediaAssetsPlayback , analytics.analyticsInsightsOverview , and analytics.analyticsInsights are removed. Upgrade: Read the new analytics through analytics.analyticsOverview , analytics.analyticsPlayersDay , and analytics.analyticsExplore . ( #822 ) Entries of playlists and of the timeline of a display know the kind adPod , the ad break with slots, spot length, and filler. Upgrade: Handle entries with kind: 'adPod' when reading playlists and timelines. ( #822 ) Added analytics.analyticsExplore.create and .export , analytics.analyticsCatalog.list and analytics.analyticsOverview.list (beta) for query, catalog, output, and overview. ( #822 ) analytics.analyticsPlayersDay.list , analytics.analyticsPlayersPlays.list , analytics.analyticsContent.list and .byId , analytics.analyticsCampaignsDelivery.list (beta). ( #822 ) fleet.playersScreenshots.list and fleet.playersScreenshots.byId (beta) read the time series of screenshots of a display. ( #822 ) analytics.analyticsProofs ( list , byId , interim ), analytics.analyticsProofsFiles.byId , and analytics.analyticsShareLinks ( list , create , remove ), all beta. ( #822 ) analytics.publicProofKeys.list , analytics.publicProofs.verify , and analytics.publicCampaignReports "
  },
  {
    "id": "changes/packages#050--2026-10-23",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.5.0 · 2026-10-23",
    "anchor": "050--2026-10-23",
    "text": "Breaking changes Das Paket nutzt das englische API-Format. Felder, Typen und Werte in Billing, Lizenzen, Abbildern und Bündeln heißen anders. Umstieg: Die Namen im eigenen Code ersetzen, etwa zielstand durch targetState und herkunft durch provenance . ( #761 ) (German only) me.apiKeys.remove(id, { reason }) verlangt den Grund owner-rotation oder compromised . Unter /v1/me antworten mit einem Schlüssel nur noch fünf Lesewege, alle übrigen mit 401. Umstieg: Den Grund mitsenden und Automation auf die fünf Lesewege beschränken. ( #762 ) (German only) Added Dokumente: Varianten page-<n> und page-<n>-sm , pageCount an Medien und der Befund DocumentPagesChanged . ( #801 ) (German only) Die Typen folgen der API: Feed-Katalog, Quellen für den Betrieb und der Fehlercode FeedSource.NotFound . ( #801 ) (German only) Neue Aufrufe: Zusammenfassung, Sammelaktion und Wiederherstellen von Benachrichtigungen, dazu state und groupKey an der Liste. ( #795 ) (German only) playerId an der Vorschau der Widget-Daten und der Lesebereich player-location . ( #801 ) (German only) Standorte: region und openingHours , dazu das Ereignis fleet.location.opening-hours-changed . ( #801 ) (German only) Widget-Definitionen des Katalogs kennen size und preview.states , Ansichten des Stores kennen size . ( #801 ) (German only) Changed Billing: zielstand heißt targetState , regel heißt rule , teilstorno heißt partialCancellation , staffel heißt tierScale , testphase heißt trialCredit . ( #761 ) (German only) Billin"
  },
  {
    "id": "changes/packages#040",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.4.0",
    "anchor": "040",
    "text": "Breaking changes GET /v1/audit/bulk/:correlationId entfällt und mit ihm client.audit.auditBulk und das Schema GetBulkActionDetailResponse . ( #742 ) (German only) Das Recht audit.bulk.read , die Ereignisart audit.bulk-action.completed und der Fehlercode Internal.AuditBulkActionDetailRepositoryUnavailable entfallen. ( #742 ) (German only)"
  },
  {
    "id": "changes/packages#030",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.3.0",
    "anchor": "030",
    "text": "Breaking changes Neue Routen der Organisation: GET /v1/me/apps/updates und GET /v1/me/apps/{appId}/update liefern die Entscheidung über ein Update (Fassung von und nach, Gründe, Neues, Änderungen in Klartext, Folgen, Frist, ob Ablehnen noch geht), POST /v1/me/apps/{appId}/update/accept und …/update/reject entscheiden mit dem Hash der Ziel-Fassung. ( #680 ) (German only) Kanäle mit Publikum: GET|PUT /v1/me/apps/beta-enrollment und POST|DELETE /v1/me/apps/{appId}/beta-enrollment tragen eine Organisation für Fassungen aus beta ein, global oder je App. Store, App-Seite, Installation und Upgrade nehmen die höchste Fassung, die die Organisation sieht, die Liste trägt dazu channel . ( #680 ) (German only) Schwimmende Pins: effectiveAppVersion(pin, installed) in @szenera/contracts/composition . Die Momentaufnahme nennt die gespielte Fassung. GET /v1/me/apps/{appId}/scene-pins und POST …/scene-pins/lift heben alle Szenen auf die installierte Fassung, je Szene als neue veröffentlichte Fassung. ( #680 ) (German only) POST /v1/marketplace/apps/{appId}/upgrade geht nur vorwärts und nur auf Fassungen, die die Organisation sieht. Neuer Fehlercode App.UpdateUnavailable (409, details.reason : not-forward , none , not-rejectable ). ( #680 ) (German only) Entfallen sind GET /v1/me/pending-reacceptances , POST /v1/marketplace/apps/{appId}/accept und …/reject samt ihren Verträgen. RetireAppReleaseRequest nimmt grace und securityAdvisory statt graceMonths . ( #680 ) (German only) Neue Ereignisse m"
  },
  {
    "id": "changes/packages#021-1",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.2.1",
    "anchor": "021-1",
    "text": "No entry for developers."
  },
  {
    "id": "changes/packages#020-1",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.2.0",
    "anchor": "020-1",
    "text": "Breaking changes Die Fähigkeiten eines Geräts kennen den Block remote nicht mehr. Die Erweiterung meldet sich im Block host mit platform: extension . Der Block host trägt dazu bridge (die Brückenfassung des Wirts) und bootStart (ob der Wirt von selbst startet). ( #640 ) (German only) Geräte tragen statt runtime den Block software (je Baustein Fassung, Bau und angenommene Freigabe), dazu channel und softwareTarget , das Soll ihres Kanals. Player tragen kein capabilities mehr, der gemeldete Zustand kein reportedState.capabilities . Was ein Gerät kann, steht in seinen Fähigkeiten, deren Block software runtime ersetzt. ( #642 ) (German only) Der Zustand der Anzeige ( lastTelemetry.renderer ) nennt das Bau-Profil ( modern oder legacy ). ( #642 ) (German only) PlayerCapabilitySchema und die beiden OS_CHANNELS entfallen, ReleaseChannel lebt in @szenera/contracts/shared neben PROTOCOL_SERVED . Die Selbstsicht trägt channel und softwareTarget . channel fehlt, solange die Cloud keinen Kanal für das Gerät führt, currentOsChannel bleibt für ältere Fernwartungen. ( #642 ) (German only) Beispiele der Vorfassungen je Naht liegen unter packages/contracts/src/samples/protocols/ , Tests in TypeScript und Go legen sie den heutigen Lesern vor. ( #642 ) (German only) HOST_EDITIONS , HostEditionSchema und HostEdition stehen in @szenera/contracts/fleet und im Einstieg @szenera/contracts/fleet/host-bridge . Die Beschreibung eines Wirts in @szenera/host-bridge nimmt edition . ( #653 ) (German only) C"
  },
  {
    "id": "changes/packages#010-1",
    "path": "/changes/packages",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "packages",
    "pageTitle": "Packages and their versions",
    "section": "0.1.0",
    "anchor": "010-1",
    "text": "Die erste Fassung mit eigener Zahl, noch nicht veröffentlicht. Diese Liste beginnt mit der nächsten Fassung. (German only)"
  },
  {
    "id": "changes/compare-specification",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Compare the specification against your version",
    "anchor": "",
    "text": "By the end, you have the specification of the current train, saved with a date, next to that of your environment, and you know whether a path has disappeared, a required field has been added, or a response carries a different status, before your integration notices. The developer site serves the specification as of the current train at /api/openapi.json . Your environment responds at /v1/openapi.json with what actually runs there. Staging and production can differ, and only the comparison tells you which change you already have in front of you and which one is still coming."
  },
  {
    "id": "changes/compare-specification#steps",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Steps",
    "anchor": "steps",
    "text": ""
  },
  {
    "id": "changes/compare-specification#load-both-specifications",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Load both specifications",
    "anchor": "load-both-specifications",
    "text": "Save the specification of the site with a date: Place the specification of your environment next to it: Note which version runs there:"
  },
  {
    "id": "changes/compare-specification#compare-version-and-paths",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Compare version and paths",
    "anchor": "compare-version-and-paths",
    "text": "Compare info.version of both files: Equal numbers do not mean equal content: info.version is an operator setting (default 1.0.0 ) and does not move with every train. So always compare the paths as well. Compare paths and methods. The command lists every operation as METHOD /path , and diff shows what is on only one side:"
  },
  {
    "id": "changes/compare-specification#check-required-fields-responses-and-stability",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Check required fields, responses, and stability",
    "anchor": "check-required-fields-responses-and-stability",
    "text": "Check the required fields of the schemas you send. The list is in required : Check the responses of the operations you use. The status codes are the keys under responses , and the error codes for them are in The error format : Check x-stability of the operations you use: The answer is stable or beta . The operation in the example is stable ."
  },
  {
    "id": "changes/compare-specification#check-the-result",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Check the result",
    "anchor": "check-the-result",
    "text": "diff in step 5 reports no line with < (only in your environment): no path that you call is removed or moved. Lines with > are new operations and break nothing. The required lists of the schemas you send contain no field that your requests do not send along. The status codes under responses of your operations are unchanged, and none carries deprecated: true . If x-stability: beta appears there, the shape can still move, see Stability classes and sunset . The dated file stays. At the next train, you reload and compare the two dated files with each other."
  },
  {
    "id": "changes/compare-specification#if-something-goes-wrong",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "If something goes wrong",
    "anchor": "if-something-goes-wrong",
    "text": "Symptom Meaning Solution Your environment still returns the old state after a train The response of /v1/openapi.json may be cached for five minutes ( Cache-Control: public, max-age=300 ). Reload after five minutes. info.version is equal, diff still shows differences The number in info.version does not follow the train. Compare the content, not the number. The running version is named by /health/version . A route is missing in both files but responds It is classified as internal and therefore not part of the public surface, in neither of the two files. Include only public operations in the integration. No promise covers an undocumented route. An operation suddenly carries deprecation headers It is being sunset. x-sunset in the description and Sunset in the response name the shutdown date. Twelve months remain: switch the call to the named successor, see Stability classes and sunset . /health/version responds with 429 The route is limited to 60 calls per minute and address. Wait the number of seconds named in Retry-After . /health/version reports unknown or a number with -dev The environment does not run from a release image. For the comparison, use the specification, not the version."
  },
  {
    "id": "changes/compare-specification#related",
    "path": "/changes/compare-specification",
    "area": "changes",
    "areaTitle": "Changes",
    "page": "compare-specification",
    "pageTitle": "Compare the specification against your version",
    "section": "Related",
    "anchor": "related",
    "text": "Release trains, notes, and deprecations Read the reference Generate your own client from the specification"
  }
]
