{"openapi":"3.1.0","info":{"title":"Nobex Streaming API","version":"2026-09-08","description":"The Nobex Streaming API lets you read and control your radio stations from your own code: list stations, read what is playing and who is listening, pull statistics and analytics, manage the music library and playlists, start or stop the station, push a now-playing title, and subscribe to events.\n\n## Authentication\n\nCreate a personal API key in the dashboard under **Developers** (`https://streaming.nobexpartners.com/developers`). The key is shown once. Send it on every request either as\n\n    Authorization: Bearer nbx_live_…\n\nor\n\n    X-API-Key: nbx_live_…\n\nKeys are account-wide — they reach every station the account owns — and carry a scope: `read` (default) or `read` + `write`. Revoke a key at any time from the same page. A dashboard session token (JWT) is also accepted and always has both scopes.\n\n## Versioning\n\nEvery response carries `X-Nobex-Api-Version: 2026-09-08`. Breaking changes ship as a new version date; additive fields do not.\n\n## Rate limits\n\n600 requests per minute per API key. Standard `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers are returned; `429` with code `rate_limited` when exceeded.\n\n## Errors\n\nEvery error is `{ \"error\": { \"code\": \"…\", \"message\": \"…\" } }`. Codes: `validation_error` (400), `unauthorized` / `invalid_api_key` / `revoked_api_key` (401), `plan_required` (402), `forbidden` / `insufficient_scope` / `account_deactivated` (403), `not_found` (404), `conflict` (409, e.g. `station_is_live`, `dmca_blocked`), `payload_too_large` (413), `rate_limited` (429), `internal_error` (500), `service_unavailable` (503).\n\n## Realtime (no key needed)\n\n`GET /public/stations/{slug}/events` is a Server-Sent Events stream. On connect it sends `now_playing` and `listeners`; afterwards every station event arrives as `event: <type>` with the event envelope as data (a `track.started` is also mirrored as a fresh `now_playing` so simple clients need only one event). A `: keepalive` comment is sent every 25 s. Reconnect with backoff if the connection drops.\n\n## Webhooks\n\nRegister an HTTPS endpoint under **Developers → Webhooks**, pick the events, and Nobex POSTs each one as JSON with an HMAC-SHA256 signature in `X-Nobex-Signature`. Events: `track.started`, `station.online`, `station.offline`, `live.started`, `live.ended`, `test.ping` (`listeners.threshold` is coming). Failed deliveries are retried after 1 m, 5 m, 30 m, 2 h and 12 h, and an endpoint that fails 100 times in a row is disabled until you re-enable it. See the `Webhooks` tag and the top-level `webhooks` section for payloads and headers.","contact":{"name":"Nobex support","email":"support@nobexinc.com","url":"https://streaming.nobexpartners.com/developers"},"x-logo":{"url":"https://streaming.nobexpartners.com/icon.png"}},"externalDocs":{"description":"Developer guide","url":"https://streaming.nobexpartners.com/developers"},"servers":[{"url":"https://streaming-api.nobexpartners.com"}],"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}],"tags":[{"name":"Meta","description":"Discovery and the account behind the key."},{"name":"Stations","description":"Your stations and their public URLs."},{"name":"Now playing","description":"What is on air, what was played, and pushing a title."},{"name":"Listeners","description":"Who is listening right now."},{"name":"Stats","description":"Listener history and analytics reports."},{"name":"Tracks","description":"The music library and direct-to-storage uploads (up to 500 MB per file)."},{"name":"Playlists","description":"Playlists and their tracks."},{"name":"Schedule","description":"The weekly programme grid."},{"name":"Control","description":"Start, stop, and put a song on air."},{"name":"Realtime","description":"Public Server-Sent Events and read-only feeds — no key required."},{"name":"Webhooks","description":"Signed HTTP callbacks for station events (configured in the dashboard)."}],"paths":{"/v1":{"get":{"tags":["Meta"],"summary":"API discovery","description":"Name, version and where the docs and OpenAPI document live.\n\n**Scope:** `read`","operationId":"getRoot","responses":{"200":{"description":"Discovery document","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"docs":{"type":"string"},"openapi":{"type":"string"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}},"/v1/openapi.json":{"get":{"tags":["Meta"],"summary":"This document","description":"The OpenAPI 3.1 description of the API. Import it into Postman, Insomnia or any OpenAPI code generator.\n\n**Scope:** `read`","operationId":"getOpenApi","responses":{"200":{"description":"OpenAPI document","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}},"/v1/me":{"get":{"tags":["Meta"],"summary":"The account behind the key","description":"Identity of the account this credential belongs to.\n\n**Scope:** `read`","operationId":"getMe","responses":{"200":{"description":"Account","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string"},"name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations":{"get":{"tags":["Stations"],"summary":"List stations","description":"Every station this account owns, newest first.\n\n**Scope:** `read`","operationId":"listStations","responses":{"200":{"description":"Stations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Station"}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}":{"get":{"tags":["Stations"],"summary":"Get a station","description":"One station by station_id or slug.\n\n**Scope:** `read`","operationId":"getStation","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Station","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Station"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/now-playing":{"get":{"tags":["Now playing"],"summary":"What is on air","description":"The same shape as the public now-playing feed, plus `source` when it can be derived.\n\n**Scope:** `read`","operationId":"getNowPlaying","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Now playing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NowPlaying"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Now playing"],"summary":"Push a now-playing title","description":"Set the title (and artist) shown to listeners and logged for reporting — for stations fed by an external encoder that does not send metadata, or a talk show naming its segment. There is no endpoint to clear a manual title: the next track or the next push replaces it.\n\n**Scope:** `write`","operationId":"setNowPlaying","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":500},"artist":{"type":"string","maxLength":500}},"required":["title"]}}}},"responses":{"200":{"description":"The recorded metadata event","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"title":{"type":"string"},"artist":{"type":["string","null"]},"source":{"type":"string","enum":["manual"]},"started_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"Missing title (`validation_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/history":{"get":{"tags":["Now playing"],"summary":"Recently played","description":"Plays newest first. The engine re-logs an unchanged song every ~90 s; consecutive duplicates are collapsed so each row is one play with its real start time.\n\n**Scope:** `read`","operationId":"getHistory","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"before","in":"query","description":"Only plays before this ISO-8601 instant (for paging backwards).","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Plays","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Play"}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/listeners":{"get":{"tags":["Listeners"],"summary":"Listener count","description":"Current listeners across every delivery path, with the per-path split. `max` is the plan cap or null when uncapped.\n\n**Scope:** `read`","operationId":"getListeners","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Listener count","content":{"application/json":{"schema":{"type":"object","properties":{"current":{"type":"integer"},"max":{"type":["integer","null"]},"by_source":{"type":"object","properties":{"icecast":{"type":"integer"},"hls":{"type":"integer"},"relay":{"type":"integer"}}},"updated_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/listeners/live":{"get":{"tags":["Listeners"],"summary":"Who is listening now","description":"One row per current session with location and device where known. `id` is an opaque per-session token; no addresses are ever returned. `unlocated` counts listeners in `total` with no session record (relay listeners, edge boxes that do not report sessions).\n\n**Scope:** `read`","operationId":"getLiveListeners","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Live listeners","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"by_source":{"type":"object","properties":{"icecast":{"type":"integer"},"hls":{"type":"integer"},"relay":{"type":"integer"}}},"data":{"type":"array","items":{"$ref":"#/components/schemas/LiveListener"}},"unlocated":{"type":"integer"},"updated_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/stats":{"get":{"tags":["Stats"],"summary":"Listener history","description":"Listener counts over a trailing window, averaged into fixed buckets (1h → 60 s, 24h → 300 s, 7d → 1800 s, 30d → 7200 s).\n\n**Scope:** `read`","operationId":"getStats","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"range","in":"query","schema":{"type":"string","enum":["1h","24h","7d","30d"],"default":"24h"}}],"responses":{"200":{"description":"Series","content":{"application/json":{"schema":{"type":"object","properties":{"range":{"type":"string"},"bucket_seconds":{"type":"integer"},"points":{"type":"array","items":{"type":"object","properties":{"t":{"type":"string","format":"date-time"},"listeners":{"type":"integer"}}}},"peak":{"type":"integer"},"average":{"type":"number"}}}}}},"400":{"description":"Unknown range (`validation_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/analytics":{"get":{"tags":["Stats"],"summary":"Analytics report","description":"The full analytics report for a date range in the station's timezone: overview (listening hours, peak, sessions, unique and returning listeners, plan usage), geography, devices, hour-by-day heatmap, top tracks and session lengths. Dates are `YYYY-MM-DD`; `end` is inclusive. Defaults to the last 30 days.\n\n**Scope:** `read`","operationId":"getAnalytics","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"start","in":"query","schema":{"type":"string","format":"date"}},{"name":"end","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Report","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AnalyticsReport"}}}}}},"400":{"description":"Malformed date (`validation_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/analytics/export.csv":{"get":{"tags":["Stats"],"summary":"Analytics CSV","description":"The same report as CSV: one row per day, then a top-tracks section.\n\n**Scope:** `read`","operationId":"exportAnalyticsCsv","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"start","in":"query","schema":{"type":"string","format":"date"}},{"name":"end","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"CSV","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/tracks":{"get":{"tags":["Tracks"],"summary":"List tracks","description":"The music library, newest upload first. `q` searches title, artist, album and filename.\n\n**Scope:** `read`","operationId":"listTracks","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"q","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Tracks","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Track"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/tracks/upload-url":{"post":{"tags":["Tracks"],"summary":"Start an upload","description":"Step 1 of 2. Returns a short-lived URL to `PUT` the file bytes to directly (send the returned `content_type` as the `Content-Type` header of that PUT). Files up to 500 MB; MP3, M4A, AAC, WAV, FLAC, OGG, OPUS, AIFF, WMA. Refused up front when the file would exceed the plan's storage allowance.\n\n**Scope:** `write`","operationId":"createUploadUrl","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","maxLength":255},"content_type":{"type":"string","description":"Advisory; the signed type is derived from the extension."},"size_bytes":{"type":"integer","minimum":1}},"required":["filename"]}}}},"responses":{"200":{"description":"Upload grant","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"track_id":{"type":"string","format":"uuid"},"upload_url":{"type":"string"},"content_type":{"type":"string"},"max_bytes":{"type":"integer"},"expires_in_seconds":{"type":"integer"}}}}}}}},"400":{"description":"Unsupported format (`validation_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Over 500 MB, or over the storage allowance (`payload_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/tracks/upload-complete":{"post":{"tags":["Tracks"],"summary":"Finish an upload","description":"Step 2 of 2, after the PUT succeeded. Records the track; size and checksum are read from the stored object. An exact duplicate of a track already in the library answers 200 with `duplicate: true` and the existing track.\n\n**Scope:** `write`","operationId":"completeUpload","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"track_id":{"type":"string","format":"uuid"},"filename":{"type":"string","maxLength":255},"title":{"type":"string","maxLength":255},"artist":{"type":"string","maxLength":255},"album":{"type":"string","maxLength":255},"isrc":{"type":"string","maxLength":32},"duration_seconds":{"type":"integer","minimum":1}},"required":["track_id","filename"]}}}},"responses":{"200":{"description":"Duplicate of an existing track","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Track"},"duplicate":{"type":"boolean","const":true}}}}}},"201":{"description":"Track recorded","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Track"},"duplicate":{"type":"boolean"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The stored object is over the limit or the storage allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/tracks/{trackId}":{"get":{"tags":["Tracks"],"summary":"Get a track","description":"One track by id.\n\n**Scope:** `read`","operationId":"getTrack","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"trackId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Track","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Track"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tracks"],"summary":"Delete a track","description":"Removes the track from the library and every playlist. The file is retired, not permanently destroyed, so a mistaken delete can be restored by support.\n\n**Scope:** `write`","operationId":"deleteTrack","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"trackId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/playlists":{"get":{"tags":["Playlists"],"summary":"List playlists","description":"Every playlist with its track count and rotation mode.\n\n**Scope:** `read`","operationId":"listPlaylists","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Playlists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Playlist"}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/playlists/{playlistId}":{"get":{"tags":["Playlists"],"summary":"Get a playlist","description":"A playlist with its tracks in order.\n\n**Scope:** `read`","operationId":"getPlaylist","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}},{"name":"playlistId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Playlist","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PlaylistDetail"}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/schedule":{"get":{"tags":["Schedule"],"summary":"Weekly schedule","description":"The programme grid keyed by day (0 = Sunday … 6 = Saturday) in the station's timezone, plus the block on air now. Same shape as the public schedule feed.\n\n**Scope:** `read`","operationId":"getSchedule","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Schedule","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"},"current":{"oneOf":[{"$ref":"#/components/schemas/ScheduleBlock"},{"type":"null"}]},"week":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleBlock"}}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/start":{"post":{"tags":["Control"],"summary":"Start the station","description":"Puts the station on air with autoDJ. Same checks as the dashboard Start button: a suspended or unentitled station is refused; `library_empty` warns that nothing is in rotation.\n\n**Scope:** `write`","operationId":"startStation","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"responses":{"200":{"description":"Started","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"on_air":{"type":"boolean"},"status":{"type":"string","enum":["starting"]},"assigned_engine":{"type":"string"},"library_empty":{"type":"boolean"}}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The station has no active plan or test window (`plan_required`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No engine is available right now (`service_unavailable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/stop":{"post":{"tags":["Control"],"summary":"Stop the station","description":"Takes the station off the air. Refused with `409 station_is_live` while a live mic is on air unless `force: true` is sent — stopping ends that broadcast.\n\n**Scope:** `write`","operationId":"stopStation","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean","default":false}}}}}},"responses":{"200":{"description":"Stopped","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"on_air":{"type":"boolean","const":false},"status":{"type":"string","enum":["stopped"]}}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A live broadcast is on air (`station_is_live`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stations/{station}/play-now":{"post":{"tags":["Control"],"summary":"Play a song now","description":"Puts a library track on air next (within ~10 s). Refused with 409 when it cannot air: `station_not_auto`, `live_active`, `show_active`, or `dmca_blocked` with the §114 violations in `details`.\n\n**Scope:** `write`","operationId":"playNow","parameters":[{"name":"station","in":"path","required":true,"description":"The station_id (`moonlightfm`) or its public slug (`moonlight-fm`) — either works.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"track_id":{"type":"string","format":"uuid"}},"required":["track_id"]}}}},"responses":{"201":{"description":"Queued","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"track_id":{"type":"string"},"eta_seconds":{"type":"integer"}}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key lacks the `write` scope (`insufficient_scope`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The pick cannot air right now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/public/stations/{slug}/events":{"get":{"tags":["Realtime"],"summary":"Server-Sent Events stream","description":"Public, no key. `text/event-stream`. On connect: `event: now_playing` (the public now-playing payload) and `event: listeners` (`{ listeners }`). Then each station event as `event: <type>` with the event envelope as data; `track.started` also re-sends `now_playing`. Keepalive comment every 25 s. At most 200 concurrent connections per API instance (503 beyond).\n\n**Scope:** `read`","operationId":"publicEvents","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Event stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Too many connections on this instance — retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}},"/public/stations/{slug}/now-playing":{"get":{"tags":["Realtime"],"summary":"Public now playing","description":"Public, no key. Poll every 10–15 s at most.\n\n**Scope:** `read`","operationId":"publicNowPlaying","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Now playing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicNowPlaying"}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}},"/public/stations/{slug}/history":{"get":{"tags":["Realtime"],"summary":"Public recently played","description":"Public, no key. Newest first, duplicates collapsed, cached 15 s.\n\n**Scope:** `read`","operationId":"publicHistory","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Plays","content":{"application/json":{"schema":{"type":"object","properties":{"station":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}}},"data":{"type":"array","items":{"type":"object","properties":{"title":{"type":["string","null"]},"artist":{"type":["string","null"]},"album":{"type":["string","null"]},"played_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Missing, invalid or revoked credential (`unauthorized`, `invalid_api_key`, `revoked_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The station (or resource) does not exist or is not owned by this account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — 600 requests per minute per key (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}}},"webhooks":{"track.started":{"post":{"tags":["Webhooks"],"summary":"A track started playing","description":"Sent when autoDJ starts a song, a live title changes, or a title is pushed manually.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTrackStarted"}}}},"responses":{"2xx":{"description":"Acknowledge with any 2xx within 10 s."}}}},"station.online":{"post":{"tags":["Webhooks"],"summary":"The station came on air","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookStationStatus"}}}},"responses":{"2xx":{"description":"Acknowledged"}}}},"station.offline":{"post":{"tags":["Webhooks"],"summary":"The station went off air","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookStationStatus"}}}},"responses":{"2xx":{"description":"Acknowledged"}}}},"live.started":{"post":{"tags":["Webhooks"],"summary":"A live broadcast started","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookLive"}}}},"responses":{"2xx":{"description":"Acknowledged"}}}},"live.ended":{"post":{"tags":["Webhooks"],"summary":"A live broadcast ended","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookLive"}}}},"responses":{"2xx":{"description":"Acknowledged"}}}},"test.ping":{"post":{"tags":["Webhooks"],"summary":"Test event","description":"Sent by the \"Send test event\" button in the dashboard.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"}}}},"responses":{"2xx":{"description":"Acknowledged"}}}}},"components":{"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"nbx_live_…","description":"Personal API key as a bearer token. Dashboard session JWTs are accepted here too."},"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Personal API key in a header, for clients that reserve Authorization for something else."}},"headers":{"X-Nobex-Api-Version":{"description":"The API version that produced this response.","schema":{"type":"string","example":"2026-09-08"}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"not_found"},"message":{"type":"string"},"details":{"description":"Extra context for some errors (validation issues, DMCA violations)."}},"required":["code","message"]}},"required":["error"]},"Station":{"type":"object","properties":{"id":{"type":"string","description":"The station_id.","example":"moonlightfm"},"slug":{"type":"string","example":"moonlight-fm"},"name":{"type":"string"},"description":{"type":["string","null"]},"genre":{"type":["string","null"]},"timezone":{"type":"string","example":"Europe/London"},"status":{"type":"string","enum":["active","suspended"]},"on_air":{"type":"boolean","description":"The engine reports the station running."},"live":{"type":"boolean","description":"A person is on the mic right now."},"stream_urls":{"type":"object","properties":{"mp3":{"type":["string","null"]},"hls":{"type":["string","null"]},"public_page":{"type":"string"}}},"logo_url":{"type":["string","null"]},"plan":{"type":"string","example":"pro"},"created_at":{"type":"string","format":"date-time"}},"required":["id","slug","name","status","on_air","live","stream_urls","plan","created_at"]},"NowPlaying":{"type":"object","properties":{"online":{"type":"boolean"},"listeners":{"type":"integer"},"max_listeners":{"type":["integer","null"]},"now_playing":{"oneOf":[{"type":"object","properties":{"title":{"type":"string"},"artist":{"type":["string","null"]},"album":{"type":["string","null"]}}},{"type":"null"}]},"source":{"type":"string","enum":["autodj","live","manual"],"description":"Absent when nothing is on."},"updated_at":{"type":"string","format":"date-time"}},"required":["online","listeners","now_playing","updated_at"]},"PublicNowPlaying":{"type":"object","properties":{"online":{"type":"boolean"},"listeners":{"type":"integer"},"max_listeners":{"type":["integer","null"]},"now_playing":{"oneOf":[{"type":"object","properties":{"title":{"type":"string"},"artist":{"type":["string","null"]},"album":{"type":["string","null"]}}},{"type":"null"}]},"updated_at":{"type":"string","format":"date-time"}}},"Play":{"type":"object","properties":{"title":{"type":["string","null"]},"artist":{"type":["string","null"]},"album":{"type":["string","null"]},"played_at":{"type":"string","format":"date-time"},"listeners":{"type":"integer","description":"Listeners when the play was logged."}},"required":["played_at","listeners"]},"LiveListener":{"type":"object","properties":{"id":{"type":"string","description":"Opaque, stable for the session."},"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2."},"city":{"type":["string","null"]},"region":{"type":["string","null"]},"device":{"type":["string","null"],"example":"mobile"},"browser":{"type":["string","null"]},"os":{"type":["string","null"]},"source":{"type":"string","enum":["icecast","hls"]},"connected_at":{"type":"string","format":"date-time"},"seconds_listening":{"type":"integer"}},"required":["id","source","connected_at","seconds_listening"]},"Track":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"]},"artist":{"type":["string","null"]},"album":{"type":["string","null"]},"isrc":{"type":["string","null"]},"duration_seconds":{"type":["integer","null"]},"file_size":{"type":"integer","description":"Bytes."},"file_format":{"type":"string","example":"mp3"},"created_at":{"type":"string","format":"date-time"}},"required":["id","file_size","created_at"]},"Playlist":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"track_count":{"type":"integer"},"mode":{"type":"string","enum":["sequential","shuffle","weighted"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","name","track_count","mode","created_at"]},"PlaylistDetail":{"allOf":[{"$ref":"#/components/schemas/Playlist"},{"type":"object","properties":{"tracks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"]},"artist":{"type":["string","null"]},"position":{"type":"integer"}}}}},"required":["tracks"]}]},"ScheduleBlock":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"day_of_week":{"type":"integer","minimum":0,"maximum":6},"start_time":{"type":"string","example":"18:00"},"end_time":{"type":"string","example":"20:00"},"source_type":{"type":"string"},"source_name":{"type":["string","null"],"description":"Only for playlist blocks — individual recordings are never pre-announced."}}},"AnalyticsReport":{"type":"object","properties":{"station_id":{"type":"string"},"timezone":{"type":"string"},"period":{"type":"object","properties":{"start":{"type":"string","format":"date"},"end":{"type":"string","format":"date"}}},"overview":{"type":"object","properties":{"listening_hours":{"type":"number"},"peak_listeners":{"type":"integer"},"peak_at":{"type":["string","null"],"format":"date-time"},"avg_listeners":{"type":"number"},"sessions":{"type":"integer"},"unique_listeners":{"type":"integer"},"avg_session_seconds":{"type":"number"},"returning_listener_pct":{"type":"number"},"hours_on_air":{"type":"number"},"previous":{"type":"object","properties":{"listening_hours":{"type":"number"},"peak_listeners":{"type":"integer"},"avg_listeners":{"type":"number"},"sessions":{"type":"integer"},"unique_listeners":{"type":"integer"},"avg_session_seconds":{"type":"number"}}},"plan":{"type":"object","properties":{"listening_hours_included":{"type":["number","null"]},"period_start":{"type":["string","null"]},"period_end":{"type":["string","null"]},"listening_hours_used_this_period":{"type":["number","null"]}}}}},"geographic":{"type":"object","properties":{"countries":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"listener_hours":{"type":"number"},"sessions":{"type":"integer"},"unique_listeners":{"type":"integer"},"pct":{"type":"number"}}}},"cities":{"type":"array","items":{"type":"object","properties":{"city":{"type":"string"},"region":{"type":["string","null"]},"country_code":{"type":"string"},"sessions":{"type":"integer"},"unique_listeners":{"type":"integer"}}}},"unlocated_pct":{"type":"number"}}},"devices":{"type":"object","properties":{"device_types":{"type":"array","items":{"$ref":"#/components/schemas/ShareRow"}},"browsers":{"type":"array","items":{"$ref":"#/components/schemas/ShareRow"}},"os":{"type":"array","items":{"$ref":"#/components/schemas/ShareRow"}},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/ShareRow"}}}},"heatmap":{"type":"object","properties":{"timezone":{"type":"string"},"cells":{"type":"array","items":{"type":"object","properties":{"dow":{"type":"integer","minimum":0,"maximum":6},"hour":{"type":"integer","minimum":0,"maximum":23},"avg_listeners":{"type":"number"},"peak_listeners":{"type":"integer"}}}}}},"top_tracks":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"title":{"type":["string","null"]},"artist":{"type":["string","null"]},"album":{"type":["string","null"]},"plays":{"type":"integer"},"avg_listeners":{"type":"number"},"max_listeners":{"type":"integer"},"last_played_at":{"type":"string","format":"date-time"}}}}}},"session_lengths":{"type":"object","properties":{"buckets":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"sessions":{"type":"integer"},"pct":{"type":"number"}}}},"median_seconds":{"type":"number"}}}},"required":[],"description":"Sections are the same as the dashboard analytics page. Until the full report is available on an account, `geographic`, `devices`, `peak_times` and `retention` may be returned instead."},"ShareRow":{"type":"object","properties":{"key":{"type":"string"},"sessions":{"type":"integer"},"pct":{"type":"number"}}},"WebhookEnvelope":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Unique per event; use it to de-duplicate retries."},"type":{"type":"string","enum":["track.started","station.online","station.offline","live.started","live.ended","test.ping"]},"created_at":{"type":"string","format":"date-time"},"api_version":{"type":"string","const":"2026-09-08"},"data":{"type":"object"}},"required":["id","type","created_at","api_version","data"],"description":"Every webhook body and every SSE `data:` payload. Headers on webhook POSTs: `Content-Type: application/json`, `User-Agent: Nobex-Webhooks/1.0`, `X-Nobex-Event: <type>`, `X-Nobex-Delivery: <delivery id>`, `X-Nobex-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>`. Reject signatures whose `t` is more than 5 minutes old."},"WebhookStationRef":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"WebhookTrackStarted":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"station":{"$ref":"#/components/schemas/WebhookStationRef"},"track":{"type":"object","properties":{"title":{"type":["string","null"]},"artist":{"type":["string","null"]},"album":{"type":["string","null"]},"isrc":{"type":["string","null"]},"source":{"type":"string","enum":["autodj","live","manual"]},"started_at":{"type":"string","format":"date-time"}}},"listeners":{"type":"integer"}}}}}]},"WebhookStationStatus":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"station":{"$ref":"#/components/schemas/WebhookStationRef"},"reason":{"type":["string","null"]},"at":{"type":"string","format":"date-time"}}}}}]},"WebhookLive":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"station":{"$ref":"#/components/schemas/WebhookStationRef"},"at":{"type":"string","format":"date-time"},"dj":{"type":["string","null"]}}}}}]}}}}