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.
7 minute readAPIUpdated: 10/25/2026
Before you start
- For
- Anyone who builds a credential into an integration or has to interpret a 401.
- Prerequisite
- an account in an organization whose package includes API access
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.
The prefixes
| 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). Exactly 46 of the 443 operations work this way.
The API key
| 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 advertising.*), the proofs (analytics.proof.read, analytics.proof.manage), as well as billing.usage.read and analytics.query.run. Not key-capable are, among others, the device list identity.device.list, the playback history fleet.playback-history.read, creating and removing displays (fleet.player.pair, fleet.player.delete), account administration identity.*, webhook administration webhook.*, and everything under platform.*. What is listed there stays reserved for sessions.
The command line shows the allow list with szenera api-key issue --list-allowed and the whole catalog with szenera permission catalog.
A key acts with the permissions of the account that created it. If that account is suspended, the key is dormant: Authentication answers 401 Auth.UserSuspended, and the key works again once the suspension is lifted. If the account is deleted, the key is revoked (reason user-deleted-cascade). The same applies to a connected application and the account that consented to it.
Under /v1/me, a key reaches only the read routes that an integration needs: GET /v1/me (who the credential is), GET /v1/me/permissions, GET /v1/me/capabilities, GET /v1/me/apps/store, and GET /v1/me/licensing. Everything else there belongs to the person and requires a session.
lastUsedAt shows the last use. The value is updated at most once per minute.
Key lifecycle
| 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 |
curl -sS -X POST "$SZENERA_API/v1/me/api-keys/akm_01KQRWJRC0APKEY00000000001/rotate" \
-H "Authorization: Bearer $SZENERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "graceHours": 24 }'
The response, with both key objects trimmed to the fields an integration needs:
{
"newApiKey": {
"id": "akm_01KQRWJRC0APKEY00000000002",
"keyPrefix": "ak_APKEY0002",
"name": "Warenwirtschaft-Anbindung",
"originClass": "read-only",
"scope": ["asset.read", "playlist.read"],
"expiresAt": "2027-05-04T07:00:00.000Z",
"revokesAt": null
},
"newPlainKey": "ak_01KQRWJRC0APKEY00000000002",
"oldApiKey": {
"id": "akm_01KQRWJRC0APKEY00000000001",
"keyPrefix": "ak_APKEY0001",
"name": "Warenwirtschaft-Anbindung",
"originClass": "read-only",
"scope": ["asset.read", "playlist.read"],
"expiresAt": "2027-05-04T07:00:00.000Z",
"revokesAt": "2026-07-21T09:15:00.000Z"
},
"oldRevokesAt": "2026-07-21T09:15:00.000Z"
}
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, 24 hours, and 3 days. How to create your first key is described in Create an API key.
The responses to an invalid key are 401 Auth.ApiKeyUnknown, 401 Auth.ApiKeyExpired, and 401 Auth.ApiKeyRevoked, all listed under Error codes: possible on any route.
The session
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 the platform-wide providers are round trips through the browser, not ways to integrate. How to set them up is described in the handbook under Organization.
Key or session?
| 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_… |