App

PUBLIC API

Developer API

Connect a server, bot, or integration to one MUSIXQUARE PRO room.

API version: v1 · Updated August 17, 2026

1. Overview

The MUSIXQUARE Developer API reads room state, controls playback and room-wide audio effects, manages the persistent playlist, and uploads audio to a PRO room. Each key is bound to one room and only receives its assigned scopes.

All versioned endpoints use this base URL:

https://api.musixquare.com/v1

This is a server-to-server API. Calls made directly from browser pages are rejected. Keep the API key in a server environment variable, never in client-side JavaScript, an app bundle, a URL, or a public repository.

Machine-readable reference: OpenAPI 3.1 specification.

2. Authentication

Send the room-bound key as a Bearer credential on every versioned request.

Authorization: Bearer mxqr_live_<key-id>.<secret>
Scopes
Keys can receive room:read, playback:read, playback:control, effects:read, effects:control, queue:read, queue:write, and media:upload. Direct file additions require both media:upload and queue:write, because completion also appends a playlist item. Effect state reads require effects:read; effect commands require effects:control.
Key safety
A key is shown only when issued. Store it as a secret, redact it from logs, and request revocation if it may have been exposed. Never send a key belonging to one room with another room code.
Authority and attribution
Treat every Developer API key as a highest-privilege room credential within its assigned scopes. API actions are room-authoritative automation, not actions whose human requester MUSIXQUARE has verified. A bot or integration must separately identify the requesting user, the integration executing the command, and the user’s exact intent. Require explicit confirmation for destructive or broadly scoped actions, and never treat another user’s message or shared conversation context as the current user’s intent.

3. Quick Start

Node.js 18 or later includes the fetch API used below.

const baseUrl = 'https://api.musixquare.com/v1';
const roomCode = process.env.MXQR_ROOM_CODE;
const apiKey = process.env.MXQR_API_KEY;

if (!roomCode || !apiKey) throw new Error('MXQR_ROOM_CODE and MXQR_API_KEY are required');

const response = await fetch(`${baseUrl}/rooms/${roomCode}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());

4. Read Room State

GET/rooms/{roomCode}
Room status, runtime, participant count, control availability, revisions, and storage quota.
GET/rooms/{roomCode}/playback
Canonical playback state, selected queue item, position, and observation time.
GET/rooms/{roomCode}/queue
Persistent playlist, current item, playlist revision, and each item's required addedBy provenance without private R2 identifiers, API key IDs, or URLs.
GET/rooms/{roomCode}/effects
Canonical full room-wide effect state, its revision, and last update time. This state remains available while the PRO room is sleeping. Send the required X-MXQR-Effects-Version: 2 header. The response uses schema version 2 and includes virtualTreble with the other four effects.
GET/rooms/{roomCode}/queue-mode
Canonical repeat and shuffle settings. Requires playback:read and remains available while the PRO room is sleeping.

addedBy is always present: participant for an item added directly in the app, current_api_key for an item added with the credential making this request, or another_api_key for an item added with a different Developer API credential. Raw API key IDs are never exposed.

Read responses include an ETag. Send it later as If-None-Match; an unchanged representation returns 304 Not Modified without a JSON body.

5. Control Playback

POST/rooms/{roomCode}/commands
Submit play, pause, seek, next, or play_item.
GET/rooms/{roomCode}/commands/{commandId}
Poll a command until it becomes applied, rejected, or expired.

Every mutation requires a unique Idempotency-Key header. Reusing the same key with the same body never repeats the operation and returns the latest sanitized state; reusing it with a different body is rejected.

const response = await fetch(`${baseUrl}/rooms/${roomCode}/commands`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({ type: 'seek', positionSeconds: 42.5 }),
});

const command = await response.json();
console.log(command.commandId, command.status);

next behaves like the app's Next button. When the selected top-level item is a YouTube playlist aggregate, it advances to the next video inside that aggregate first. After the aggregate's last video, it advances to the next top-level item using the room's current repeat and shuffle settings. play_item is different: it force-selects the exact top-level item identified by queueItemId instead of traversing from the current item's internal video.

Playback and effect commands are authorized and applied by the PRO room server. They remain available while the room runtime is sleeping; its timeline stays frozen until participants return and consume the canonical state. Selecting a large file can be committed before every connected or returning device has finished downloading and decoding it, so read playback state before issuing an immediately dependent command.

6. Control Audio Effects

Submit set_effects through the same command endpoint and poll the returned command ID through /commands/{commandId}. The request is a partial update: omitted effects and omitted reverb fields keep their current values. All supplied fields are validated and applied as one command. The effects object and every supplied nested object must contain at least one field.

const response = await fetch(`${baseUrl}/rooms/${roomCode}/commands`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    type: 'set_effects',
    effects: {
      reverb: {
        mixPercent: 40,
        decaySeconds: 1,
        preDelaySeconds: 0.02,
        lowCutPercent: 0,
        highCutPercent: 0,
      },
      equalizer: { bandsDb: [5, 3, 0, -2, -3] },
      virtualBass: { strengthPercent: 60 },
      virtualSurround: { widthPercent: 120 },
      virtualTreble: { enabled: true },
    },
  }),
});

const command = await response.json();
console.log(command.commandId, command.status);
Reverb
mixPercent 0–100, decaySeconds 0.1–30, preDelaySeconds 0–1, and lowCutPercent / highCutPercent 0–100. The app's Studio preset is { mixPercent: 40, decaySeconds: 1, preDelaySeconds: 0.02, lowCutPercent: 0, highCutPercent: 0 }. Arena is { mixPercent: 50, decaySeconds: 5, preDelaySeconds: 0.12, lowCutPercent: 0, highCutPercent: 40 }. Turn reverb off with { mixPercent: 0 }.
Equalizer
bandsDb must contain exactly five values from -12 through 12 dB, ordered at 60, 230, 910, 3,600, and 14,000 Hz. Bright is [0, -2, 0, 4, 6]; Warm is [5, 3, 0, -2, -3]; flat/off is [0, 0, 0, 0, 0].
Virtual bass
strengthPercent accepts 0–100. The app's on value is 60; off is 0.
Virtual surround
widthPercent accepts 0–200 and controls stereo width, not channel routing. The app's on value is 120; neutral/off is 100.
Virtual treble
enabled is a boolean. Use true to turn the room-wide exciter on and false to turn it off. Every effects read uses version 2 and includes this field.

Effect state is persisted across PRO room sleep/wake cycles and client reconnects, and joining devices receive the current state. set_effects is applied directly by the room server even while the runtime is sleeping; there is no browser coordinator. Read /effects after the command becomes applied to observe the canonical result.

The in-room BOT reads this same canonical effect state and can turn virtual treble on or off only when the user makes an explicit immediate request.

These are room-wide Web Audio effects. They do not control device-local volume, channel roles, subwoofer cutoff, or manual sync. YouTube playback is unaffected. During system-audio sharing, the sender hears the original source while receivers hear the configured effects.

7. Manage the Playlist

POST/rooms/{roomCode}/queue/items
Add one independent YouTube video, or include playlistId to add one YouTube playlist aggregate whose videoId is its entry video.
POST/rooms/{roomCode}/queue/items/batch
Atomically add 1–100 YouTube items in order. If one item is invalid or the complete batch does not fit, none are added. The effective limit is whichever comes first: 100 items or a 128 KiB complete JSON request. Each distinct playlistId creates exactly one top-level playlist: if the same ID appears again, the first occurrence supplies its position and display metadata while manifest-less rows contribute their videoId in request order, or identical full manifests are preserved once. Mixed or conflicting manifests are rejected. Duplicate IDs are preserved. Entries without playlistId remain independent videos, including repeated IDs.
DELETE/rooms/{roomCode}/queue/items
Clear the entire playlist atomically with no request body. Warning: this removes tracks added by people and by every API credential. It stops current playback, clears the current selection, and returns one updated queue with no items.
DELETE/rooms/{roomCode}/queue/items/owned
Atomically remove only items added by the API credential making this request, with no request body. An empty match is a successful no-op. Playback stops and the selection clears only when the current item belongs to this credential; released media references become eligible for later R2 cleanup.
DELETE/rooms/{roomCode}/queue/items/{queueItemId}
Remove only the identified queue item with no request body. Removing media also releases its playlist reference for later cleanup.
PUT/rooms/{roomCode}/queue/order
Replace the order with queueItemIds, an exact permutation of the current queue, and the basePlaylistRevision returned by the latest queue read.
PUT/rooms/{roomCode}/queue-mode
Replace repeat and shuffle settings with an explicit desired state. Requires playback:control, a unique Idempotency-Key, and the revision returned by the latest queue-mode read.
await fetch(`${baseUrl}/rooms/${roomCode}/queue/items`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    videoId: '<youtube-video-id>',
    name: 'Example video',
    title: 'Example title',
    artist: 'Example artist',
  }),
});
// Add one YouTube playlist aggregate with its complete ordered manifest.
await fetch(`${baseUrl}/rooms/${roomCode}/queue/items`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    videoId: '<playlist-entry-video-id>',
    videoIds: ['<playlist-entry-video-id>', '<next-video-id>'],
    playlistId: '<youtube-playlist-id>',
    name: 'Example playlist',
    title: 'Example playlist',
  }),
});
const batchResponse = await fetch(
  `${baseUrl}/rooms/${roomCode}/queue/items/batch`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify({
      items: videos.map(({ videoId, title, artist }) => ({
        videoId,
        name: title,
        title,
        artist,
      })),
    }),
  },
);

if (!batchResponse.ok) throw new Error(await batchResponse.text());
const queueAfterBatch = await batchResponse.json();

Batch canonicalization follows request order. For every non-empty playlistId, the first item supplies the aggregate's position, entry videoId, and display metadata; later items with that same ID are absorbed and their metadata is ignored. Manifest-less rows contribute their lone videoId in request order; repeated full videoIds manifests must be identical and are preserved once. A manifest accepts 1–5,000 IDs, preserves duplicates, and must contain the item's entry videoId. Mixed manifest forms are rejected. Every supplied item is still validated atomically, but the number of appended top-level items can therefore be smaller than items.length.

const clearResponse = await fetch(`${baseUrl}/rooms/${roomCode}/queue/items`, {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});

if (!clearResponse.ok) throw new Error(await clearResponse.text());

const clearedQueue = await clearResponse.json();
console.log(clearedQueue.currentQueueItemId); // null
console.log(clearedQueue.items.length); // 0
// Safe bot cleanup: human tracks and tracks from other API keys remain.
const cleanupResponse = await fetch(`${baseUrl}/rooms/${roomCode}/queue/items/owned`, {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});

if (!cleanupResponse.ok) throw new Error(await cleanupResponse.text());

const queueAfterCleanup = await cleanupResponse.json();
console.log(queueAfterCleanup.items);
const queue = await fetch(`${baseUrl}/rooms/${roomCode}/queue`, {
  headers: { Authorization: `Bearer ${apiKey}` },
}).then((response) => response.json());

await fetch(`${baseUrl}/rooms/${roomCode}/queue/order`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    basePlaylistRevision: queue.playlistRevision,
    queueItemIds: queue.items.map((item) => item.queueItemId).reverse(),
  }),
});
const queueMode = await fetch(`${baseUrl}/rooms/${roomCode}/queue-mode`, {
  headers: { Authorization: `Bearer ${apiKey}` },
}).then((response) => response.json());

const queueModeResponse = await fetch(`${baseUrl}/rooms/${roomCode}/queue-mode`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    baseRevision: queueMode.revision,
    repeatMode: 'all',
    shuffleEnabled: true,
  }),
});

if (queueModeResponse.status === 409) {
  // Read the latest state, decide again, then retry with its revision and a new key.
  throw new Error('Queue mode changed concurrently');
}

const updatedQueueMode = await queueModeResponse.json();

Collection DELETE /queue/items clears every person's and every integration's item; DELETE /queue/items/owned removes only items owned by the current API credential; item DELETE /queue/items/{queueItemId} removes exactly one. All three require a unique Idempotency-Key for each new intent. Replaying a key returns the current sanitized queue without applying that mutation again. Reordering rejects a stale playlist revision; fetch the queue again instead of retrying an old order blindly. Queue writes remain available while the room is sleeping and do not start playback automatically. A full clear always stops current playback; an owned cleanup stops it only when it removes the selected item.

Queue mode is an explicit setter, not a toggle: every update supplies baseRevision, repeatMode (off, all, or one), and shuffleEnabled. On 409 QUEUE_MODE_REVISION_CONFLICT, read /queue-mode again, reconsider the desired state, and retry with the fresh revision and a new idempotency key. Use the same key only when replaying the exact same request after an uncertain transport result.

Queue-mode reads and writes work while the PRO room is sleeping, and the stored state applies on its next wake. The exact shuffle order is server-owned and intentionally hidden: it is preserved while shuffle stays enabled, generated when shuffle changes from off to on, and cleared when shuffle is disabled.

8. Upload Audio

Uploads use a three-step flow. The audio body goes directly to a short-lived signed R2 URL and never passes through the Developer API Worker.

A reservation requires name, byteLength, and mime. Optional metadata is title, artist, thumbnail, and a lowercase 64-character sha256 hint.

POST/rooms/{roomCode}/media/uploads
Reserve capacity and receive an opaque asset ID, signed PUT URL, exact required headers, uploadExpiresAtMs, and the later completionExpiresAtMs grace deadline.
PUT{upload.url}
Send the exact declared number of bytes directly to R2 using every returned header.
POST/rooms/{roomCode}/media/uploads/{assetId}/complete
Verify the object, finalize it, and append exactly one audio item to the playlist.
const reservationResponse = await fetch(
  `${baseUrl}/rooms/${roomCode}/media/uploads`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify({
      name: 'music.flac',
      byteLength: audioBuffer.byteLength,
      mime: 'audio/flac',
      title: 'Example title',
      artist: 'Example artist',
    }),
  },
);

const reservation = await reservationResponse.json();

await fetch(reservation.upload.url, {
  method: reservation.upload.method,
  headers: reservation.upload.headers,
  body: audioBuffer,
  redirect: 'manual',
});

await fetch(
  `${baseUrl}/rooms/${roomCode}/media/uploads/${reservation.assetId}/complete`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Idempotency-Key': crypto.randomUUID(),
    },
  },
);

Treat the signed upload URL as a temporary credential. Do not log it, follow redirects, alter its query string, omit returned headers, or send a different content length. Completing an upload appends the item but never starts playback automatically.

Audio files deliberately do not use a binary-in-JSON batch endpoint. Each file keeps its own reservation, direct R2 upload, completion, checksum, quota check, and idempotency key. Multiple files can use the same three-step helper with at most two in flight per API key:

const pending = [...audioFiles];
const workers = Array.from(
  { length: Math.min(2, pending.length) },
  async () => {
    while (pending.length > 0) {
      const file = pending.shift();
      await reservePutAndComplete(file); // the three-step flow above
    }
  },
);

await Promise.all(workers);

Multi-file audio upload is intentionally not atomic: a file that completes remains in the playlist if a later file fails. Record each returned asset and queue item so your integration can retry or remove it explicitly.

9. Limits and Supported Media

Storage
Up to 200 MiB per asset and 1 GiB of persistent media per PRO room.
Upload issuance
Up to two active reservations per key and ten new reservations per hour.
Audio candidates
MP3, WAV, FLAC, M4A, AAC, OGG/OGA, Opus, WebM audio, AIF/AIFF, and CAF are accepted as candidates. Actual decoding support still depends on each playback device and browser.
Request rate
Read and mutation limits are applied per key and room. A 429 response includes Retry-After; wait for that duration before retrying.

10. Responses and Errors

JSON responses include X-Request-Id. Keep that value when reporting a failure. Error responses use a stable code, human-readable message, request ID, and retryable flag.

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Try again later.",
    "requestId": "req_<request-id>",
    "retryable": true
  }
}

Immediate non-2xx responses use the uppercase codes below. A command that was accepted with 202 is different: poll it and inspect its lowercase resultCode. Never treat a lowercase command result as an HTTP error code. Retry automatically only when retryable is true, and honor Retry-After when it is present.

Immediate HTTP error codes

API_DISABLED · 503 · retryable: true
The Developer API is disabled. Retry with backoff or contact the room operator if it persists.
API_NOT_CONFIGURED · 503 · retryable: true
The API deployment is temporarily incomplete or unavailable. Retry with backoff.
ASSET_CAPACITY_EXCEEDED · 409 · retryable: false
The room cannot register another stored media asset. Remove unused file-backed items before trying a new upload.
BACKEND_UNAVAILABLE · 503 · retryable: true
A required MUSIXQUARE backend did not respond. Retry with backoff and retain the request ID if the failure continues.
BROWSER_ORIGIN_FORBIDDEN · 403 · retryable: false
The request came directly from a browser origin. Move the API call and key to a trusted server.
COMMAND_CAPACITY_EXCEEDED · 409 · retryable: false
Too many playback commands are still active. Let existing commands settle, read current state, then submit a new intent with a new idempotency key.
COORDINATOR_INCOMPATIBLE · 409 · retryable: false
Legacy v1 compatibility value. Current PRO rooms are server-authoritative and do not use a browser coordinator. If an older or transitioning backend returns this code, read current state and update the integration before retrying.
FORBIDDEN · 403 · retryable: false
The key lacks a required scope. Use a key issued with the required permission; repeating the same request will not help.
IDEMPOTENCY_CONFLICT · 409 · retryable: false
The idempotency key was already used with a different request. Reconcile current state before creating a new key for a new intent.
IDEMPOTENCY_KEY_REQUIRED · 400 · retryable: false
A mutation is missing a valid Idempotency-Key. Add a unique key that satisfies the documented header format.
INTERNAL_RESPONSE_INVALID · 503 · retryable: true
A backend returned a response that failed the public contract check. Retry with backoff and retain the request ID.
INVALID_REQUEST · 400 · retryable: false
The request body, header, value, declared upload, or size is invalid. Correct it against the OpenAPI schema before resubmitting.
NOT_FOUND · 404 · retryable: false
The route, room, queue item, command, asset, or upload is unavailable to this key. Re-read the relevant collection and identifiers.
NO_MEDIA · 409 · retryable: false
No playable item is currently selected. Choose a queue item in the app or submit play_item with a valid queueItemId.
PLAYLIST_CAPACITY_EXCEEDED · 409 · retryable: false
The persistent playlist is full. Remove queue items before adding more.
PLAYLIST_REVISION_CONFLICT · 409 · retryable: false
The playlist changed after the supplied base revision. Read the queue again, recompute the mutation, and use a new idempotency key.
QUEUE_MODE_REVISION_CONFLICT · 409 · retryable: false
Repeat or shuffle state changed after the supplied base revision. Read /queue-mode again before sending a new explicit update.
RATE_LIMITED · 429 · retryable: true
The key, room, or ingress limit was exceeded. Wait for the response's Retry-After duration before retrying.
RESERVATION_CAPACITY_EXCEEDED · 409 · retryable: false
This key has too many unfinished upload reservations. Complete outstanding uploads or wait for abandoned reservations to expire.
ROOM_QUOTA_EXCEEDED · 409 · retryable: false
The upload would exceed the PRO room storage quota. Remove stored file items before reserving another upload.
ROOM_SLEEPING · 409 · retryable: false
Legacy v1 compatibility value. The current server-authoritative path accepts canonical playback and effect commands while the room runtime is sleeping. If this code is observed, read room state and treat the responding backend as incompatible or in transition.
ROOM_STATE_CAPACITY_EXCEEDED · 409 · retryable: false
The bounded persistent room record cannot accept this change. Reduce playlist or stored state before submitting a new mutation.
UNAUTHORIZED · 401 · retryable: false
The Bearer key is missing, malformed, expired, revoked, or unknown. Obtain a valid room-bound key.
UPLOAD_INCOMPLETE · 409 · retryable: true
The signed direct upload is not visible as complete yet. Honor Retry-After, then retry completion for the same reservation.
UPLOAD_MISMATCH · 409 · retryable: false
The uploaded object does not match its reservation. Verify size, digest, and upload target, then create a fresh reservation if needed.

Accepted command result codes

A successful POST /commands returns 202 and a command ID. Poll that command until it is terminal. Before submitting another intent after any non-success result, read canonical playback and queue state first. Result names containing coordinator are retained only for v1 wire compatibility; current PRO rooms do not elect a browser coordinator.

applied
The room server committed the requested intent to canonical state. Connected or returning devices may still be loading the selected media.
already_applied
The canonical room state already matched the requested intent; treat this as success.
busy
The room server was handling an incompatible transition. Read current state before deciding whether a new command is still needed.
no_media
The selected media was no longer available when the room server applied the accepted command.
stale_queue
The current queue item changed between command acceptance and execution. Read the queue and playback state again.
unsupported_mode
The requested operation is not supported by the current media mode.
expired
The accepted command did not reach a terminal server result before its deadline.
execution_failed
The room server received the command but could not complete the media operation.
coordinator_changed
Legacy v1 compatibility value for a superseded browser-coordinator command. Current server-authoritative rooms do not emit it for ordinary command handling.
coordinator_incompatible
Legacy v1 compatibility value for an unsupported browser-coordinator control version. Current server-authoritative rooms do not require that browser role.
coordinator_unavailable
Legacy v1 compatibility value for loss of a browser coordinator. Current server-authoritative rooms continue without one.

11. Compatibility and Contact

The URL major version is v1. New optional response fields may be added compatibly; clients should ignore fields they do not recognize. Removing a field or changing its meaning requires a new major version.

For API key issuance, revocation, or integration questions, contact [email protected].