{"components":{"schemas":{"Error":{"properties":{"error":{"properties":{"code":{"type":"string"},"message":{"type":"string"}},"type":"object"}},"type":"object"},"Session":{"properties":{"code":{"description":"Display code for the room URL. Not a secret.","type":"string"},"cohosts":{"items":{"$ref":"#/components/schemas/User"},"type":"array"},"created":{"type":"number"},"deckCount":{"enum":[2,4]},"endReason":{"type":["string","null"]},"ended":{"type":["number","null"]},"genres":{"items":{"type":"string"},"type":"array"},"host":{"$ref":"#/components/schemas/User"},"id":{"type":"string"},"listeners":{"type":"integer"},"media":{"enum":["flowing","stalled","none"]},"nowPlaying":{"items":{"properties":{"artist":{"type":"string"},"deck":{"type":"integer"},"title":{"type":"string"}},"type":"object"},"type":"array"},"peakListeners":{"type":"integer"},"started":{"type":["number","null"]},"status":{"enum":["starting","live","reconnecting","ended"]},"title":{"type":"string"},"url":{"type":"string"},"visibility":{"enum":["public","unlisted","private"]}},"type":"object"},"SessionSettings":{"properties":{"allow":{"description":"Handles allowed into a private session.","items":{"type":"string"},"type":"array"},"b2b":{"description":"Back-to-back code, to broadcast a B2B.","type":"string"},"deckCount":{"enum":[2,4]},"genres":{"items":{"type":"string"},"maxItems":4,"type":"array"},"privacy":{"properties":{"showCues":{"type":"boolean"},"showUpcoming":{"type":"boolean"},"showWaveforms":{"type":"boolean"}},"type":"object"},"quality":{"enum":["high","standard","low"]},"title":{"maxLength":80,"type":"string"},"visibility":{"enum":["public","unlisted","private"]}},"type":"object"},"Tokens":{"properties":{"accessToken":{"type":"string"},"expiresIn":{"type":"integer"},"refreshToken":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}},"type":"object"},"User":{"properties":{"color":{"type":"string"},"display":{"type":"string"},"handle":{"type":"string"},"id":{"type":"string"}},"type":"object"}},"securitySchemes":{"bearer":{"description":"Access token (1 h) from /auth/login, /auth/register or /auth/refresh.","scheme":"bearer","type":"http"},"cookie":{"description":"The website's sign-in cookie; changes also need X-CSRF-Token.","in":"cookie","name":"session","type":"apiKey"},"scope":{"description":"A session token from /sessions/{sid}/join (listener 15 min, publisher 2 h).","scheme":"bearer","type":"http"}}},"info":{"description":"Session lifecycle, discovery, tokens and waveform assets for SONORA broadcasts. Realtime state and audio use Socket.IO on /socket.io/ (see the protocol section of /docs and PROTOCOL.md). Errors are `{\"error\": {\"code\", \"message\"}}` with a matching HTTP status. Browser pages authenticate with the site cookie plus an `X-CSRF-Token` header; SONORA uses `Authorization: Bearer <accessToken>`.","title":"SONORA Live API","version":"1"},"openapi":"3.1.0","paths":{"/auth/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"device":{"type":"string"},"handle":{"type":"string"},"password":{"type":"string"}},"required":["handle","password"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tokens"}}},"description":"Signed in"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Wrong handle or password"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited"}},"summary":"Sign in (desktop)"}},"/auth/logout":{"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"refreshToken":{"type":"string"}},"required":[],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"ok"}},"summary":"Revoke a refresh token"}},"/auth/refresh":{"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"refreshToken":{"type":"string"}},"required":["refreshToken"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tokens"}}},"description":"New tokens"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Sign in again"}},"summary":"Exchange a refresh token (it is rotated: the old one stops working)"}},"/auth/register":{"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"device":{"type":"string"},"display":{"type":"string"},"handle":{"type":"string"},"password":{"minLength":8,"type":"string"}},"required":["handle","password"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tokens"}}},"description":"Signed in"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid handle or password"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited"}},"summary":"Create an account (desktop)"}},"/b2b":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{sessions}"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Back-to-back requests and sessions of the caller"},"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"decks":{"enum":["2","4"]},"partner":{"type":"string"},"title":{"type":"string"}},"required":["partner"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session}"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Ask another DJ to go back to back"}},"/b2b/{code}/tracks/{key}":{"get":{"responses":{"200":{"description":"application/octet-stream"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Fetch the other DJ's track"},"put":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{ok}"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Pass a loaded track to the other DJ (binary body, \u2264 120 MB, deleted when the B2B ends)"}},"/b2b/{code}/{action}":{"post":{"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}},{"in":"path","name":"action","required":true,"schema":{"enum":["accept","decline","end"]}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session}"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not now"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"accept | decline | end"}},"/health":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"ok"}},"summary":"Service health"}},"/me":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Account"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not signed in"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"The signed-in account"}},"/nettest":{"post":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{bytes, serverSeconds}"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Upload test: post up to 4 MB, time it on the client"}},"/sessions":{"get":{"parameters":[{"in":"query","name":"q","schema":{"type":"string"}},{"in":"query","name":"genre","schema":{"type":"string"}},{"in":"query","name":"cursor","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"maximum":60,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{items: Session[], nextCursor, total}"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limited"}},"summary":"Public broadcasts that are starting, live or reconnecting"},"post":{"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionSettings"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session, resumed: true}"},"201":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session, resumed: false}"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The other B2B DJ is broadcasting (`owner`)"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Create a broadcast (or carry on the host's open one: `resumed: true`)"}},"/sessions/{sid}":{"get":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session}"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or private"}},"summary":"A session the caller may see"},"patch":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionSettings"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session}"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not the host"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Ended"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Change title, genres, visibility, allow list, privacy or quality (host)"}},"/sessions/{sid}/assets/{asset}":{"put":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}},{"in":"path","name":"asset","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"analysisVersion":{"type":"string"},"bands":{"description":"Base64 of one byte (0\u2013255) per point per band","properties":{"high":{"type":"string"},"low":{"type":"string"},"mid":{"type":"string"}},"type":"object"},"duration":{"type":"number"},"gen":{"type":"integer"},"rate":{"description":"Points per second","type":"number"}},"required":["rate","bands"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{ok, stored, length}"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Too large"},"507":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Quota"}},"security":[{"scope":[]}],"summary":"Upload a loaded track's waveform (publisher token)"}},"/sessions/{sid}/assets/{asset}/chunks/{level}/{index}":{"get":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}},{"in":"path","name":"asset","required":true,"schema":{"type":"string"}},{"in":"path","name":"level","required":true,"schema":{"type":"integer"}},{"in":"path","name":"index","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{gen, level, index, start, bands}"}},"security":[{"scope":[]}],"summary":"4096 points of one level"}},"/sessions/{sid}/assets/{asset}/manifest":{"get":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}},{"in":"path","name":"asset","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"manifest"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No waveform"}},"security":[{"scope":[]}],"summary":"Waveform levels, chunk counts and a 1024-point overview (listener or publisher token)"}},"/sessions/{sid}/chat":{"get":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{items}"}},"summary":"The last 100 chat messages"}},"/sessions/{sid}/end":{"post":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{session}"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not the host"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"End the broadcast (host). Voids every token of the session."}},"/sessions/{sid}/fallback":{"get":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}},{"in":"query","name":"t","schema":{"type":"string"}}],"responses":{"200":{"description":"audio/mpeg or audio/ogg, unbounded"}},"summary":"MP3 (or Ogg Opus) stream for browsers without WebCodecs; estimated timing only (listener token as `t`)"}},"/sessions/{sid}/history":{"get":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{items}"}},"summary":"Tracks that were audible, oldest first"}},"/sessions/{sid}/join":{"post":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"role":{"enum":["listener","publisher"]}},"required":[],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{token, expiresIn, role, session, protocolVersion, realtime, fallback}"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a DJ of this session"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or private"},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Ended"}},"summary":"A scoped token: `listener` (anyone who may see the session) or `publisher` (its DJs)"}},"/sessions/{sid}/moderation":{"post":{"parameters":[{"description":"Session id (s_\u2026) or room code.","in":"path","name":"sid","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"action":{"enum":["delete","mute","unmute","mod","unmod"]},"handle":{"type":"string"},"messageId":{"type":"string"}},"required":["action"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"ok"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not host or moderator"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Delete a message, mute/unmute a listener, add/remove a moderator"}},"/time":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{ts}"}},"summary":"Server time in ms (for clock offset estimates)"}},"/users/{handle}":{"get":{"parameters":[{"in":"path","name":"handle","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"profile"}},"summary":"A DJ's public profile"}},"/users/{handle}/follow":{"post":{"parameters":[{"in":"path","name":"handle","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"on":{"type":"boolean"}},"required":[],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"{following, followers}"}},"security":[{"bearer":[]},{"cookie":[]}],"summary":"Follow or unfollow"}}},"servers":[{"url":"/api/v1"}]}
