{"openapi":"3.1.0","info":{"title":"Online Radio API","version":"1.0.0","description":"Public station data needs no key. Private data (`read`) and control (`control`) need a station API key created under Manage station → Integrations, sent as `Authorization: Bearer rp_...`. Times are ISO 8601 UTC. Webhook payloads are signed: `X-Radio-Signature: sha256=HMAC_SHA256(secret, X-Radio-Timestamp + \".\" + body)`."},"servers":[{"url":"https://cfbtradio.com"}],"components":{"securitySchemes":{"stationKey":{"type":"http","scheme":"bearer","description":"Station API key (rp_...)"}},"schemas":{"Stream":{"type":"object","properties":{"format":{"enum":["mp3","aac","hls"]},"bitrate_kbps":{"type":"integer"},"url":{"type":"string","format":"uri"}}},"Station":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"tagline":{"type":"string"},"language":{"enum":["th","en"]},"timezone":{"type":"string"},"genre":{"type":"string"},"province":{"type":"string"},"url":{"type":"string","format":"uri"},"embed_url":{"type":"string","format":"uri"},"logo":{"type":["object","null"],"properties":{"url":{"type":"string"},"alt":{"type":"string"}}},"streams":{"type":"array","items":{"$ref":"#/components/schemas/Stream"}},"on_air":{"type":"boolean"},"now_playing":{"$ref":"#/components/schemas/Track"}}},"Track":{"type":["object","null"],"properties":{"title":{"type":"string"},"artist":{"type":"string"},"source":{"enum":["live","autodj","scheduled","fallback"]},"started_at":{"type":"string","format":"date-time"},"show":{"type":["string","null"]}}},"NowPlaying":{"type":"object","properties":{"on_air":{"type":"boolean"},"source":{"type":["string","null"]},"now_playing":{"$ref":"#/components/schemas/Track"},"show":{"type":["object","null"],"properties":{"name":{"type":"string"},"host":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"}}},"listeners":{"type":"integer"},"capacity":{"type":["integer","null"]},"alerts":{"type":"array","description":"Active emergency alerts for this station","items":{"type":"object","properties":{"id":{"type":"string"},"severity":{"enum":["extreme","severe","moderate","minor","test"]},"title":{"type":"string"},"message":{"type":"string"},"instruction":{"type":"string"},"language":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}}}}}},"Airing":{"type":"object","properties":{"show":{"type":"string"},"host":{"type":"string"},"kind":{"enum":["live","prerecorded","playlist","relay"]},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"}}},"Play":{"type":"object","properties":{"title":{"type":"string"},"artist":{"type":"string"},"source":{"type":"string"},"started_at":{"type":"string","format":"date-time"}}},"Episode":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"show":{"type":["string","null"]},"published_at":{"type":"string","format":"date-time"},"duration_s":{"type":"number"},"url":{"type":"string"},"audio_url":{"type":"string"},"transcript_url":{"type":["string","null"]}}},"Stats":{"type":"object","description":"Same numbers as the statistics page: summary, byDay, byHour, byStream, byClient, byCountry, byLength, topSongs, topShows"},"Webhook":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Delivery payload id (idempotency key)"},"event":{"enum":["track.started","live.started","live.ended","request.received","episode.published","listeners.peak","station.silence"]},"station_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object"}}}}},"paths":{"/api/v1/stations":{"get":{"summary":"Station directory","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search (Thai-aware: missing tone marks, wrong keyboard layout, romanized)"},{"name":"genre","in":"query","schema":{"type":"string"}},{"name":"province","in":"query","schema":{"type":"string"}},{"name":"language","in":"query","schema":{"enum":["th","en"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Station"}}}}}}}}}},"/api/v1/stations/{slug}":{"get":{"summary":"Station details and stream URLs","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Station"}}}},"404":{"description":"Station not found"},"429":{"description":"Rate limited"}}}},"/api/v1/stations/{slug}/nowplaying":{"get":{"summary":"What is on air now","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NowPlaying"}}}},"404":{"description":"Station not found"},"429":{"description":"Rate limited"}}}},"/api/v1/stations/{slug}/schedule":{"get":{"summary":"Upcoming schedule","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"},{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":31,"default":7}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Airing"}}}}}}}}}},"/api/v1/stations/{slug}/history":{"get":{"summary":"Recently played","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Play"}}}}}}}}}},"/api/v1/stations/{slug}/episodes":{"get":{"summary":"Published on-demand episodes","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Episode"}}}}}}}}}},"/api/v1/stations/{slug}/stats":{"get":{"summary":"Listener statistics","security":[{"stationKey":["read"]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stats"}}}},"401":{"description":"Missing or invalid key"},"404":{"description":"Station not found"},"429":{"description":"Rate limited"}}}},"/api/v1/stations/{slug}/listeners":{"get":{"summary":"Current listeners per stream","security":[{"stationKey":["read"]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"}],"responses":{"200":{"description":"OK"},"401":{"description":"Missing or invalid key"}}}},"/api/v1/stations/{slug}/skip":{"post":{"summary":"Skip the current Auto DJ track","security":[{"stationKey":["control"]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"}],"responses":{"200":{"description":"Skipped"},"401":{"description":"Missing or invalid key"},"409":{"description":"A DJ is live"},"502":{"description":"Streaming engine unreachable"}}}},"/api/public/stations/{slug}/events":{"get":{"summary":"Server-Sent Events stream of now-playing changes (event: nowplaying)","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,40}$"},"description":"Station slug"}],"responses":{"200":{"description":"text/event-stream"}}}}},"webhooks":{"stationEvent":{"post":{"summary":"Station event (see Webhook schema)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"}}}},"responses":{"200":{"description":"Any 2xx acknowledges; others are retried 5 times with back-off"}}}}}}