ERICADE Radio API v2 — Station Data WebSocket

Live push events for both stations: now playing, next up, song requests, star ratings, playlist positions and live overrides.
See also the REST API v2 documentation. (Updated: 2026-07-30)

Overview

The station data WebSocket lets the website receive events the moment they happen, instead of polling /radio/v2/nowplaying on a timer. One connection carries events for both stations; every event states the StationID and StationName it belongs to, so a client can show either station (or both) from a single socket.

WSS wss://api.ericade.net/ws/stationdata

Six station events are pushed:

EventSent whenCarries full track data?
nowplayingA new song starts playing on a station, or a live override begins.Yes (except live overrides)
nextupA new “next song” appears for a station.Yes
songrequestedA listener request is accepted on a station.Yes
ratingsetA star rating is set for a track.Yes
NowPlayingOnPlaylistThe playlist position of a running show changes.No — the playlist entry is a plain text field.
OverrideA station goes live (override on) or returns to normal broadcast operations (override off).No

Authentication: none. The socket is read-only and carries the same public data as the REST endpoints. Clients cannot inject data through it — the only accepted client messages are the control actions listed below.

Connecting

Open the socket, optionally subscribe to a subset of stations, and handle incoming JSON messages. Immediately after connecting the server sends a welcome message followed by a replay of the latest known nowplaying, nextup and NowPlayingOnPlaylist events (marked "Replay": 1), so a client is usually up to date without any REST call.

Example (browser JavaScript)

let ws;

function connect() {
  ws = new WebSocket("wss://api.ericade.net/ws/stationdata");

  ws.onopen = () => {
    // Optional: only receive events for station 1.
    ws.send(JSON.stringify({ Action: "subscribe", StationID: [1] }));
  };

  ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    switch (msg.Event) {
      case "nowplaying":
        console.log(`[${msg.StationName}] Now playing:`,
                    msg.Payload.Track ? msg.Payload.Track.Title : msg.Payload.Title);
        break;
      case "nextup":
        console.log(`[${msg.StationName}] Next up:`, msg.Payload.Title);
        break;
      case "songrequested":
        console.log(`[${msg.StationName}] ${msg.Payload.Requester} requested ${msg.Payload.Title}`);
        break;
      case "ratingset":
        console.log(`[${msg.StationName}] ${msg.Payload.Title} rated ${msg.Payload.Stars} stars`);
        break;
    }
  };

  // Reconnect with backoff; see "Client best practices".
  ws.onclose = () => setTimeout(connect, 2000 + Math.random() * 3000);
}

connect();

Message envelope

Every message from the server — station events and control messages alike — is a JSON object with this envelope:

AttributeDescriptionExample
EventMessage type. Station events: nowplaying, nextup, songrequested, ratingset, NowPlayingOnPlaylist, Override. Control messages: welcome, subscribed, pong, error.nowplaying
StationIDStation the event belongs to. 0 for control messages, which concern the connection rather than a station.1 = ericade.radio
2 = Best of ericade.radio
StationNameStation name in plain text. Empty for control messages.24/7 tracked music
TimestampWhen the event occurred, Unix epoch.1785440425
TimestampHRWhen the event occurred, human-readable server local time.2026-07-30 21:00:25
ReplayPresent and set to 1 when the message is a cached snapshot replayed after connect/subscribe, rather than a freshly fired event. Absent on live events.1
PayloadEvent-specific data, see each event below. Absent on some control messages.Object

Station events

nowplaying

Sent when a new song starts playing on a station. The Payload.Track object contains the complete track data — the same object the Now Playing and Track by ID REST endpoints return (Artist, Title, Slug, Image, CompositeRating, TrackArtists, Playlog and so on). See the REST documentation for the full field list.

Payload attributeDescriptionExample
TrackIDTrack identifier. 0 when the track could not be resolved (rare; e.g. live broadcasts).14906
ArtistComplete artist field as ingested. For listener requests this includes the request prefix.Dr. Future
TitleTrack title.Plastic pop
GuidInternal GUID of the media file.4f2c…
isListenerRequested1 when this play is a listener request, otherwise 0. Use this instead of the field inside Track, which is always 0.0 or 1
StartedAtWhen the song started, Unix epoch. Remember to apply Track.WebStreamingOffset before flipping the display, exactly as with the REST endpoint.1785440425
TrackFull track object (see above). Omitted if the track lookup failed; fall back to the flat fields in that case.Object

Example

{
  "Event": "nowplaying",
  "StationID": 1,
  "StationName": "24/7 tracked music",
  "Timestamp": 1785440425,
  "TimestampHR": "2026-07-30 21:00:25",
  "Payload": {
    "TrackID": 14906,
    "Artist": "Dr. Future",
    "Title": "Plastic pop",
    "Guid": "4f2c8c31-6b2e-4a44-9d0a-1c2f3e4d5a6b",
    "isListenerRequested": 0,
    "StartedAt": 1785440425,
    "Track": {
      "Artist": "Dr. Future",
      "Title": "Plastic pop",
      "StationID": 1,
      "StationName": "24/7 tracked music",
      "Slug": "https://ericade.radio/#/song/14906/plastic-pop-dr-future",
      "Image": "https://radio.ericade.net/images/demoscene.png",
      "CompositeRating": 4.2,
      "PlayLength": 213.4,
      "TrackArtists": [ { "ArtistID": 4821, "Artist": "Dr. Future" } ]
    }
  }
}
The Track object above is heavily shortened. The real object contains every field documented for the Now Playing response.
Live overrides: when a station goes live (see the Override event), a nowplaying event fires whose data comes from the station’s settings table instead of the track database: TrackID is 0, isOverride is 1, there is no Track object, and the flat Artist/Title/Image fields carry the free-text override values. Display those as they are.

nextup

Sent when a new “next song” appears for a station. Payload attributes:

Payload attributeDescriptionExample
TrackIDTrack identifier of the upcoming song. For listener requests this is resolved to the real track.10663
ArtistArtist of the upcoming song. For listener requests the request prefix is stripped.Cube
TitleTitle of the upcoming song.My pixels are weapons
GuidInternal GUID of the media file.9a1b…
TrackFull track object, same shape as for nowplaying. Omitted if the lookup failed.Object
The relay filters nextup events in two ways:
1. Buffer window: a nextup arriving within 5 seconds after the last nowplaying for the same station is discarded — listeners are still hearing the previous song then, and flipping the next-up display that early would spoil it. A discarded event is forgotten completely, so a later resend of the same track is still broadcast.
2. Duplicate suppression: if the next-up slot for a station is set to the same track it already holds (matched by TrackID, or by Artist/Title/Guid when the track could not be resolved), no new event is broadcast. The cached snapshot replayed to newly connected clients is likewise left untouched, so it still reflects the timestamp of the last genuine change rather than a repeat call.

songrequested

Sent when a listener request passes all checks and is queued. The greeting and the requester’s IP are deliberately not broadcast. Payload attributes:

Payload attributeDescriptionExample
TrackIDTrack identifier of the requested song.10663
ArtistArtist of the requested song.Dr Future
TitleTitle of the requested song.Plastic pop (Plastic kills life mix)
RequesterName the requester entered.Erik
QueuePositionPosition in the request queue after this request was added.3
ExpectedWaitTimeHuman-readable estimate of the time until play.48 minutes
TrackFull track object, same shape as for nowplaying.Object

ratingset

Sent when a listener sets a star rating on a track. The voter’s IP and browser hash are deliberately not broadcast. Payload attributes:

Payload attributeDescriptionExample
TrackIDTrack identifier of the rated song.14906
ArtistArtist of the rated song.Dr. Future
TitleTitle of the rated song.Plastic pop
StarsThe rating that was just given, 1–5.5
CompositeRatingNew composite rating for the track after this vote.4.31
VotesTotal number of votes for the track after this vote.17
TrackFull track object with the fresh rating, same shape as for nowplaying.Object
ratingset concerns the track that was rated, which is not necessarily the track currently playing. Match on Payload.TrackID before updating any “now playing” UI.

NowPlayingOnPlaylist

Sent when the playlist position of a running show changes. Shows (for example live sets or podcast episodes) carry a PlayList text field with timestamped entries; while such a show is playing, the relay re-evaluates the current entry every 5 seconds and fires this event whenever it changes. The value is a plain text field — there is no artist or song data behind it. Present it as it is.

Payload attributeDescriptionExample
NowPlayingOnPlaylistThe playlist entry that is playing right now, exactly as written in the show’s playlist. May be empty when no entry matches the current position.Jester of Elysium - Elysium

Example

{
  "Event": "NowPlayingOnPlaylist",
  "StationID": 1,
  "StationName": "24/7 tracked music",
  "Timestamp": 1785441106,
  "TimestampHR": "2026-07-30 21:11:46",
  "Payload": {
    "NowPlayingOnPlaylist": "Jester of Elysium - Elysium"
  }
}
The starting value is already included in the nowplaying event as Track.NowPlayingOnPlaylist; this event only fires for subsequent changes. The latest value is cached and replayed to newly connecting clients (marked "Replay": 1) for as long as the show is playing; the cache is cleared when the next song starts.
Podcast stations: on station 2 the “song” is a podcast episode that does not change for a long time, so the relay does not rely on nowplaying events alone there: it also actively checks every 5 seconds which track is currently playing and picks up the running episode’s playlist by itself — for example after a relay restart in the middle of an episode. The playlist position is polled continuously either way, so NowPlayingOnPlaylist events keep flowing throughout the whole episode.

Override

Sent when a station’s override state changes in the settings table — that is, when a live broadcast takes over the station (override on) or the station returns to normal broadcast operations (override off). The event only fires on an actual state change, not on every call to the settings endpoint.

Payload attributeDescriptionExample
OverrideThe new override state. 1 = live override active, 0 = normal broadcast.0 or 1
MessageHuman-readable state description: Just went live when the override turns on, Normal broadcast operations when it turns off.Just went live
ArtistOverride artist text from the settings table. Only present when Override is 1.ericade.radio
TitleOverride title text from the settings table. Only present when Override is 1.Live from the demo party!
ImageOverride image URL from the settings table. Only present when Override is 1.https://radio.ericade.net/images/live.png

Example

{
  "Event": "Override",
  "StationID": 1,
  "StationName": "24/7 tracked music",
  "Timestamp": 1785442000,
  "TimestampHR": "2026-07-30 21:26:40",
  "Payload": {
    "Override": 1,
    "Message": "Just went live",
    "Artist": "ericade.radio",
    "Title": "Live from the demo party!",
    "Image": "https://radio.ericade.net/images/live.png"
  }
}
When the override turns on, a nowplaying event with the override data from the settings table fires alongside this event (see the note in the nowplaying section). When the override turns off, only the Override event fires — the next regular nowplaying arrives when the playout system starts the next song. Clients that want the current track immediately can make one REST call to /radio/v2/nowplaying on receiving Override: 0.

Control messages

Control messages use StationID: 0 and an empty StationName.

welcome

First message after the connection is established.

AttributeDescriptionExample
ProtocolProtocol version. Bumped when the message format changes.1
StationsArray of available stations: StationID and StationName.[{"StationID":1,"StationName":"24/7 tracked music"}, …]
DocumentationURL of this page.

subscribed

Acknowledges a subscribe action. Subscribed lists the station IDs the connection now receives. After the acknowledgement, cached nowplaying/nextup/NowPlayingOnPlaylist messages for the chosen stations are replayed (marked "Replay": 1).

pong

Answer to the application-level ping action.

error

Sent when a client message could not be processed. Message contains a human-readable explanation. The connection stays open.

Client actions

Clients may send the following JSON messages. Anything else is answered with an error message.

subscribe

Restrict (or widen) which stations the connection receives events for. Without a subscribe, a connection receives all stations.

{ "Action": "subscribe", "StationID": [1, 2] }
AttributeMandatory?DescriptionNotes
ActionYesMust be subscribe.
StationIDYesArray of station IDs (a single integer is also accepted). An empty array means all stations.Unknown IDs are rejected with an error and the previous subscription is kept.

snapshot

Asks the server to resend the cached nowplaying/nextup/NowPlayingOnPlaylist messages (marked "Replay": 1) for the subscribed stations. Useful after a mobile browser wakes a background tab.

{ "Action": "snapshot" }

ping

Application-level keepalive for clients that cannot send protocol-level pings (browsers). The server answers with pong. Not normally needed — the server pings every client at the protocol level anyway.

{ "Action": "ping" }

Client best practices

TopicRecommendation
Initial stateRely on the replayed messages after welcome. If none arrive within a couple of seconds (the relay was just restarted and has not seen a song change yet), fall back to one REST call to /radio/v2/nowplaying.
ReconnectingReconnect on close with a randomized backoff (e.g. 2–5 s, doubling up to ~60 s). Do not hammer the server with immediate retries.
Buffer offsetApply Track.WebStreamingOffset (seconds) before flipping the now-playing display, exactly as when polling. The event fires when the playout system starts the song, which is earlier than listeners hear it.
Replay flagTreat "Replay": 1 messages as state synchronization, not as fresh events — do not re-trigger animations or notifications for them.
Keep polling as fallbackThe WebSocket is an optimization. Keep a slow REST poll (for example every few minutes) or use the Sequence mechanism as a safety net if your use case cannot tolerate a missed event.
Unknown eventsIgnore Event values you do not recognize; new event types may be added without a protocol bump.

Limits

LimitValueWhat happens when exceeded
Simultaneous clients200New connections are refused with HTTP 503.
Simultaneous clients per IP20New connections are refused with HTTP 503.
Client messages20 per 10 secondsConnection is closed (code 1008).
Client message size4 KBConnection is closed (code 1009).
KeepaliveProtocol ping every 30 sClients that miss two pings in a row are dropped (code 1001).
Message typesText frames with JSON onlyBinary frames close the connection (code 1003).

These values are configurable server-side; see Operations.

Operations (station staff)

This section documents how the WebSocket endpoint works internally and how to run it. API consumers can stop reading here.

Architecture

Apache/PHP cannot hold WebSocket connections, so the endpoint is a separate long-running CLI daemon, websocket/stationdata-server.php (“the relay”), fronted by Apache. The flow is:

  1. An API endpoint does its normal work — updatetrack() ingests a new play, nextup() stores the next song, addrequest() accepts a request, updatestar() stores a rating, Settings::Override() flips the override state of a station.
  2. At the end of that work it calls PublishStationEvent() (in config/common.php), which sends one small UDP datagram to the relay on loopback. This is fire-and-forget: if the relay is down the datagram is lost and the API call is completely unaffected.
  3. The relay authenticates the datagram with the shared key, gates nextup events (buffer window and duplicate suppression, see the nextup section), enriches events that carry a TrackID with the full track data through the existing objGetSong() object (no duplicated SQL), caches the latest nowplaying/nextup/NowPlayingOnPlaylist per station for replay, and broadcasts the result to all subscribed WebSocket clients.
  4. One event type originates in the relay itself: while the current track of a station carries a PlayList, the relay re-evaluates the playlist position every 5 seconds through the existing ReturnPlayListData() function (a pure time computation, no database work) and broadcasts a NowPlayingOnPlaylist event whenever the position changes.
  5. For the stations listed in $WebSocketPollNowPlaying (station 2, whose “songs” are long podcast episodes), the relay additionally checks every 5 seconds which track is currently playing (one cheap SELECT against the nowplaying table) and re-establishes the playlist watch through objGetSong() when the track changed — so the running episode’s playlist is followed even if the relay started mid-episode. An unchanged episode costs only the SELECT; the full track lookup runs once per episode change.

Configuration (config.php)

SettingDefaultDescription
$WebSocketEnabled1Master switch for event publishing. 0 also makes the relay refuse to start.
$WebSocketListenAddress127.0.0.1Address the relay listens on for WebSocket clients. Keep on loopback; Apache proxies wss:// to it.
$WebSocketListenPort8146TCP port for WebSocket clients.
$WebSocketEventHost / $WebSocketEventPort127.0.0.1 / 8147Where PublishStationEvent() sends the internal UDP datagrams.
$WebSocketEventKey(secret)Shared secret authenticating internal datagrams. Change before deploying.
$WebSocketMaxClients / $WebSocketMaxClientsPerIP200 / 20Connection limits.
$WebSocketPingInterval30Seconds between keepalive pings.
$WebSocketPollNowPlayingarray(2)Stations where the relay actively checks the currently playing track every 5 seconds, instead of relying on nowplaying events alone. Use for stations that play long podcast episodes.

Apache

Enable mod_proxy and mod_proxy_wstunnel, then add to the api.ericade.net TLS vhost:

ProxyPass        "/ws/stationdata" "ws://127.0.0.1:8146/"
ProxyPassReverse "/ws/stationdata" "ws://127.0.0.1:8146/"

TLS terminates in Apache; the relay itself speaks plain TCP on loopback only.

Running the relay

The watchdog script websocket/stationdata-ws.sh starts the relay if it is not running and removes duplicates, following the same pattern as the Discord bot. Add to root’s crontab:

* * * * * /var/www/html/api.ericade.net/websocket/stationdata-ws.sh

The relay logs to /var/log/stationdata-ws.txt (via the watchdog’s redirection). A plain HTTP GET /health against the relay port answers 200 OK and can be used for monitoring:

curl -s http://127.0.0.1:8146/health
After a relay restart the replay cache is empty until the next song change on each station. Clients fall back to one REST call in that window (see Client best practices), so no operator action is needed.
The websocket/ directory is blocked from web access by its .htaccess. Keep it that way — the relay must only ever run as a CLI daemon.