Skip to content

API Keys

An API key is a long-lived credential tied to a specific user account. It lets scripts, home-automation systems, dashboards, and other integrations call the MZAP API without going through the interactive login flow.

Each user has at most one API key. The key inherits the user's role and assignments — an Operator's key can do exactly what that Operator can do in the UI, nothing more. Keys do not expire, but admins can regenerate (which invalidates the previous key) or delete the user (which revokes the key permanently).

API keys look like this:

mzap_f4e3a09b12c4d7e56890abcdef0123456789fedcba0987654321

The prefix mzap_ followed by 48 hex characters makes the key easy to detect in logs and easy to match with regular expressions. Keys are generated from 24 cryptographically random bytes.

  1. Open the Users tab as an admin.
  2. Find the user's row.
  3. Click Regenerate Key. The new key replaces any existing one and is copied to your clipboard automatically.

There is no "create once, hide forever" flow — the full key is visible on the Users tab to admins, so you can recover it if you lose the clipboard copy. If that is a concern for your deployment, regenerate the key after distributing it and treat the admin account as the keystore.

To revoke a key without creating a new one, regenerate it and don't store the new value, or disable/delete the user account.

Send the key on every request in one of these ways:

GET /api/players HTTP/1.1
Host: localhost:5000
X-API-Key: mzap_f4e3a09b12c4d7e56890abcdef0123456789fedcba0987654321
Terminal window
curl -H "X-API-Key: mzap_..." http://localhost:5000/api/players

Session tokens and API keys are both accepted on the Authorization: Bearer header, so any client library that already speaks OAuth-style bearer auth works unchanged.

Authorization: Bearer mzap_f4e3a09b12c4d7e56890abcdef0123456789fedcba0987654321

Useful when the client can't set custom headers — for example, passing an SSE URL directly to EventSource in the browser, or embedding a stream URL in another tool.

http://localhost:5000/api/stream/events?apiKey=mzap_...

Prefer headers when possible. Query parameters show up in server logs and browser histories.

A key carries the same permissions as its user, evaluated at request time (not at key creation time). If you change the user's role or assignments, the key's effective permissions change with it.

Practical implications:

  • Issuing an Operator key for a home-automation integration limits the blast radius if the key leaks — it can only touch the players and zones you assigned.
  • Issuing a Viewer key for a status dashboard prevents the dashboard from ever changing audio, even if it's compromised.
  • Use the Admin role for keys only when you genuinely need full control (e.g. a provisioning script).

A disabled user's key is rejected (401 Unauthorized). Re-enabling the user restores the key.

GET /api/auth/status answers without credentials, so a client can find out whether authentication is on before it has any. When the request carries an X-API-Key header or an Authorization: Bearer token, the response also reports who that credential belongs to:

{
"authEnabled": true,
"loggedIn": true,
"user": {
"id": "...",
"username": "lounge-panel",
"role": "Operator",
"assignedPlayerIds": ["..."],
"assignedZoneIds": ["..."],
"hiddenFeatures": []
}
}

With no credential, or an invalid one, it returns loggedIn: false and user: null — never 401. Use this to confirm a stored key still works and to read its role and assignments.

#!/usr/bin/env bash
BASE="http://localhost:5000"
KEY="mzap_..."
# List players visible to this key
curl -s -H "X-API-Key: $KEY" "$BASE/api/players" | jq '.[] | {id, name}'
# Set the volume of a specific player to 50%
curl -s -X PUT \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"volume": 0.5}' \
"$BASE/api/players/<player-id>/volume"
const key = 'mzap_...';
const events = new EventSource(
`http://localhost:5000/api/stream/events?apiKey=${encodeURIComponent(key)}`,
);
// Every event is sent with a named `event:` field, so `onmessage` never fires.
// Register a listener for each event type you care about.
const types = [
'PlayerStateChangedEvent',
'PlayerVolumeChangedEvent',
'ZoneVolumeChangedEvent',
'RoutingChangedEvent',
'JingleStateChangedEvent',
'PlaylistChangedEvent',
'WorkspaceChangedEvent',
];
for (const type of types) {
events.addEventListener(type, (e) => {
const evt = JSON.parse(e.data);
console.log(evt.eventType, evt);
});
}

Each message looks like this on the wire:

event: PlayerStateChangedEvent
data: {"eventType":"PlayerStateChangedEvent","timestamp":"2026-09-24T10:15:00Z","playerId":"...", ...}

The data payload is flat camelCase JSON: eventType and timestamp are always present, alongside the event's own fields. Enum fields are serialized as numbers.

The server also sends two kinds of SSE comment line, which carry no data and which EventSource ignores:

  • : connected — sent once, as soon as the stream opens, so a client that waits for body bytes knows the connection is live even on a quiet host.
  • : heartbeat — sent every 60 seconds.

When a player moves to a different track, the stream carries a PlaylistChangedEvent for its playlist in addition to the PlayerStateChangedEvent. Use it to move a "now playing" highlight that follows the playlist's currentIndex. It is sent only when the position actually changes — including auto-advance, Next, Previous, jumping to a track, and a Sequential playlist reaching its end.

The stream only emits events for players and zones in the key-holder's assignment list.

GET /api/stream/levels sends about ten meter frames a second. A slower client can ask for fewer with the optional intervalMs query parameter:

http://localhost:5000/api/stream/levels?intervalMs=250&apiKey=mzap_...
  • Accepted range is 100–2000 ms; values outside it are clamped. Leaving it out (or sending an unparseable value) keeps the default of 100 ms.
  • The rate applies only to that connection — other clients keep their own rate.
  • The first frame is always sent straight away, so meters appear as soon as the client connects.
  • Treat API keys like passwords. Store them in your secrets manager or environment variables, not in source control.
  • MZAP does not currently rate-limit authentication attempts. If you expose the API on an untrusted network, put it behind a reverse proxy that enforces rate limits and TLS.
  • There is no audit log for per-request key usage in the current version. Use request logging on your reverse proxy if you need one.