API reference
ERICADE Radio API v2 — Station Data WebSocket
Live push events for both stations: now playing, next up, song requests, star ratings, playlist positions, live overrides and messages from the station. See also the REST API v2 documentation, and the chat WebSocket — a second socket, built like this one, for which a visitor has to log in.
Updated: 2026-09-19
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.
Eight station events are pushed. Six carry track data, two carry words:
| Event | Sent when | Carries full track data? |
|---|---|---|
now-playing | A new song starts playing on a station, or a live override begins. | Yes (except live overrides) |
next-up | A new “next song” appears for a station. | Yes |
song-request | A listener request is accepted on a station. | Yes |
song-rating | A star rating is set for a track. | Yes |
now-playing-on-playlist | The playlist position of a running show changes. | No — the playlist entry is a plain text field. |
override | A station goes live (override on) or returns to normal broadcast operations (override off). | No |
station-event | The station has something to say to its listeners: a message meant to be shown on websites. | No — severity, source, subject and text. |
system-event | A message for administrators and their tools. Not meant for display to listeners. | No — same fields as station-event. |
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. That includes the system-event: it is not shown on
the station’s website, but it is not secret — every client receives it.
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 now-playing, next-up and now-playing-on-playlist 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 "now-playing":
console.log(`[${msg.StationName}] Now playing:`,
msg.Payload.Track ? msg.Payload.Track.Title : msg.Payload.Title);
break;
case "next-up":
console.log(`[${msg.StationName}] Next up:`, msg.Payload.Title);
break;
case "song-request":
console.log(`[${msg.StationName}] ${msg.Payload.Requester} requested ${msg.Payload.Title}`);
break;
case "song-rating":
console.log(`[${msg.StationName}] ${msg.Payload.Title} rated ${msg.Payload.Stars} stars`);
break;
case "station-event":
// Words for listeners. The text is HTML-escaped plain text - see "Displaying the text".
showToListeners(msg.Payload.Severity, messageText(msg.Payload.Subject), messageText(msg.Payload.Message));
break;
case "system-event":
// For administrators' tools. A page for listeners does nothing here.
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:
| Attribute | Description | Example |
|---|---|---|
Event | Message type. Station events: now-playing, next-up, song-request, song-rating, now-playing-on-playlist, override, station-event, system-event. Control messages: welcome, subscribed, pong, error. | now-playing |
StationID | Station the event belongs to. 0 for control messages, which concern the connection rather than a station — and for a station-event or system-event addressed to the whole network, which every client receives whatever it subscribed to. | 1 = ericade.radio 2 = Best of ericade.radio |
StationName | Station name in plain text. Empty for control messages and whenever StationID is 0. | 24/7 tracked music |
Timestamp | When the event occurred, Unix epoch. | 1785440425 |
TimestampHR | When the event occurred, human-readable server local time. | 2026-07-30 21:00:25 |
Replay | Present 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 |
Payload | Event-specific data, see each event below. Absent on some control messages. | Object |
Station events
now-playing
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 — and, for a podcast episode that has been scanned,
FileData: its length, loudness and ID3 tags, and the address of its
waveform chart). See the REST documentation for the full field list.
| Payload attribute | Description | Example |
|---|---|---|
TrackID | Track identifier. 0 when the track could not be resolved (rare; e.g. live broadcasts). | 14906 |
Artist | Complete artist field as ingested. For listener requests this includes the request prefix. | Dr. Future |
Title | Track title. | Plastic pop |
Guid | Internal GUID of the media file. | 4f2c… |
isListenerRequested | 1 when this play is a listener request, otherwise 0. Use this instead of the field inside Track, which is always 0. | 0 or 1 |
StartedAt | When the song started, Unix epoch. Remember to apply WebStreamingOffset before flipping the display, exactly as with the REST endpoint. | 1785440425 |
WebStreamingOffset | Seconds to delay the display by to compensate for stream buffering. Always present, even when there is no Track object (e.g. live overrides). | 12 |
Track | Full track object (see above). Omitted if the track lookup failed; fall back to the flat fields in that case. | Object |
Example
{
"Event": "now-playing",
"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,
"WebStreamingOffset": 12,
"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" } ]
}
}
}
Track object above is heavily shortened. The real object contains every field
documented for the Now Playing response.now-playing 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.next-up
Sent when a new “next song” appears for a station. Payload attributes:
| Payload attribute | Description | Example |
|---|---|---|
TrackID | Track identifier of the upcoming song. For listener requests this is resolved to the real track. | 10663 |
Artist | Artist of the upcoming song. For listener requests the request prefix is stripped. | Cube |
Title | Title of the upcoming song. | My pixels are weapons |
Guid | Internal GUID of the media file. | 9a1b… |
WebStreamingOffset | Seconds to delay the display by to compensate for stream buffering. Same meaning as on now-playing. | 12 |
Track | Full track object, same shape as for now-playing. Omitted if the lookup failed. | Object |
next-up events in two ways:
1. Buffer window: a
next-up arriving within 5 seconds after the last
now-playing 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.song-request
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 attribute | Description | Example |
|---|---|---|
TrackID | Track identifier of the requested song. | 10663 |
Artist | Artist of the requested song. | Dr Future |
Title | Title of the requested song. | Plastic pop (Plastic kills life mix) |
Requester | Name the requester entered. | Erik |
QueuePosition | Position in the request queue after this request was added. | 3 |
ExpectedWaitTime | Human-readable estimate of the time until play. | 48 minutes |
Track | Full track object, same shape as for now-playing. | Object |
song-rating
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 attribute | Description | Example |
|---|---|---|
TrackID | Track identifier of the rated song. | 14906 |
Artist | Artist of the rated song. | Dr. Future |
Title | Title of the rated song. | Plastic pop |
Stars | The rating that was just given, 1–5. | 5 |
CompositeRating | New composite rating for the track after this vote. | 4.31 |
Votes | Total number of votes for the track after this vote. | 17 |
Track | Full track object with the fresh rating, same shape as for now-playing. | Object |
song-rating concerns the track that was rated, which is not necessarily the track
currently playing. Match on Payload.TrackID before updating any “now playing” UI.now-playing-on-playlist
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 attribute | Description | Example |
|---|---|---|
NowPlayingOnPlaylist | The 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": "now-playing-on-playlist",
"StationID": 1,
"StationName": "24/7 tracked music",
"Timestamp": 1785441106,
"TimestampHR": "2026-07-30 21:11:46",
"Payload": {
"NowPlayingOnPlaylist": "Jester of Elysium - Elysium"
}
}
now-playing 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.now-playing 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 now-playing-on-playlist 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 attribute | Description | Example |
|---|---|---|
Override | The new override state. 1 = live override active, 0 = normal broadcast. | 0 or 1 |
Message | Human-readable state description: Just went live when the override turns on, Normal broadcast operations when it turns off. | Just went live |
Artist | Override artist text from the settings table. Only present when Override is 1. | ericade.radio |
Title | Override title text from the settings table. Only present when Override is 1. | Live from the demo party! |
Image | Override 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"
}
}
now-playing event with the override
data from the settings table fires alongside this event (see the note in the
now-playing section). When the override turns off, only the
override event fires — the next regular now-playing 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.Messages: station-event and system-event
Two events carry words instead of track data. They share one payload and differ in whom they are for:
| Event | For | A website for listeners… | Example |
|---|---|---|---|
station-event | Listeners and everybody using the websites. | shows it. radio.ericade.net does, on every open page. | The stream moves to a new server at 22:00 CET. |
system-event | Administrators and their tools: dashboards, bots, monitoring. | ignores it. It must not appear there. | Playout restarted. |
system-event reaches every client exactly as a station-event does. “Not shown on the website” is not “secret”, and the sender is held to that: a message carries no address and nothing about a listener or an account, and IPv4/IPv6 addresses that do turn up in a text are replaced by [address removed] before it is broadcast.Messages are sent by an administrator through /radio/v2/message (the form is radio.ericade.net/conelrad/message/), by the broadcast automation server, or by the API’s own code (PublishStationMessage() in config/common.php). They are never cached and never replayed: a client that connects a second later does not get them, and a snapshot does not bring them back. Timestamp / TimestampHR in the envelope say when the message was sent. StationID is 0 for the whole network — every client gets it — or one station, for the clients following it.
Payload
Payload attributes, the same for station-event and system-event:
| Payload attribute | Description | Default | Example |
|---|---|---|---|
Severity | One of information, low, medium, high, critical — least to most serious. Always one of these five, in lower case. | information | medium |
Source | Who is speaking: the broadcast automation server, or the public display name of the administrator who chose to sign the message. At most 64 characters. Text, escaped. | LeisaWolfe | LeisaWolfe |
Subject | A short subject line: one line, at most 120 characters. Text, escaped. | Station message / System message | Maintenance tonight |
Message | The message itself: at most 4096 characters (before escaping), line breaks as \n. May be empty. Text, escaped. | empty | The stream moves to a new server at 22:00 CET. |
Example
{
"Event": "station-event",
"StationID": 0,
"StationName": "",
"Timestamp": 1789725600,
"TimestampHR": "2026-09-18 12:00:00",
"Payload": {
"Severity": "medium",
"Source": "LeisaWolfe",
"Subject": "Maintenance tonight",
"Message": "The stream moves to a new server at 22:00 CET.\nExpect a short break & thank you for waiting <3"
}
}
Displaying the text
Subject, Message and Source are plain text and never markup — there are no tags, no links and no formatting in these events, and there never will be. They are valid UTF-8 without control characters or Unicode direction overrides, and they arrive HTML-escaped: the five characters & < > " ' travel as & < > " ', as in the example above. That is a seat belt: a client that carelessly assigns the text to innerHTML still shows text. It is no reason to be careless.
| Do | Do not |
|---|---|
Undo exactly that escaping — the five entities, in one pass — and assign the result to textContent (or hand it to your framework’s text binding). | Decode the text and then assign it to innerHTML, v-html, dangerouslySetInnerHTML or a string-built attribute. That removes the seat belt and drives into the wall. |
Keep line breaks with white-space: pre-line, and let long words wrap (overflow-wrap: anywhere): a message may hold a 300-character address. | Turn \n into <br> by string replacement. |
Treat an unknown Severity as information. | Build a class name or a file name out of it without checking it against the five values. |
Show a station-event to listeners. | Show, log to a visible console or forward a system-event on a page listeners see. |
// One pass, so that "&lt;" - somebody typed "<" - comes out as "<" and not as "<".
function messageText(s) {
const map = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
return typeof s !== "string" ? "" :
s.replace(/&(amp|lt|gt|quot|apos|#0*39|#[xX]0*27);/g, (whole, name) => map[name] || "'");
}
subjectElement.textContent = messageText(msg.Payload.Subject);
bodyElement.textContent = messageText(msg.Payload.Message); // CSS: white-space: pre-line
+ to space): it would turn “1+1” into “1 1” and choke on “100%”. The reference client at radio.ericade.net/wbreference.html shows both kinds of text handled side by side.Control messages
Control messages use StationID: 0 and an empty StationName.
welcome
First message after the connection is established.
| Attribute | Description | Example |
|---|---|---|
Protocol | Protocol version. Bumped when the message format changes. | 1 |
Stations | Array of available stations: StationID and StationName. | [{"StationID":1,"StationName":"24/7 tracked music"}, …] |
Documentation | URL of this page. |
subscribed
Acknowledges a subscribe action. Subscribed lists the station IDs the connection now
receives. After the acknowledgement, cached now-playing/next-up/now-playing-on-playlist 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] }
| Attribute | Mandatory? | Description | Notes |
|---|---|---|---|
Action | Yes | Must be subscribe. | |
StationID | Yes | Array 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 now-playing/next-up/now-playing-on-playlist 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
| Topic | Recommendation |
|---|---|
| Initial state | Rely 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. |
| Reconnecting | Reconnect 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 offset | Apply Payload.WebStreamingOffset (seconds) before flipping the now-playing display, exactly as when polling. It is present even for live overrides, where there is no Track object. The event fires when the playout system starts the song, which is earlier than listeners hear it. |
| Replay flag | Treat "Replay": 1 messages as state synchronization, not as fresh events — do not re-trigger animations or notifications for them. |
| Keep polling as fallback | The 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 events | Ignore Event values you do not recognize; new event types may be added without a protocol bump — as station-event and system-event were. |
| Messages | Show a station-event, as text; do not show a system-event to listeners. See Displaying the text. Neither is replayed, so there is nothing to catch up on after a reconnect. |
| Station filter | If you filter incoming events by StationID, let 0 through: it marks control messages and messages addressed to the whole network. |
Limits
| Limit | Value | What happens when exceeded |
|---|---|---|
| Simultaneous clients | 200 | New connections are refused with HTTP 503. |
| Simultaneous clients per IP | 20 | New connections are refused with HTTP 503. |
| Client messages | 20 per 10 seconds | Connection is closed (code 1008). |
| Client message size | 4 KB | Connection is closed (code 1009). |
| Keepalive | Protocol ping every 30 s | Clients that miss two pings in a row are dropped (code 1001). |
| Message types | Text frames with JSON only | Binary frames close the connection (code 1003). |
These values are configurable server-side; see Operations.
Operations
The relay exposes two plain-HTTP endpoints on the same TCP port as the WebSocket listener (before the upgrade handshake), for use by uptime monitors and dashboards — not by station-data clients.
GET /health
Liveness check. Always returns 200 OK with the body OK as long as the relay's
event loop is running. Does not reflect capacity.
GET /status
Current connection capacity, as plain text:
Used websockets: 42, Available websockets: 158, Total websockets: 200
| Field | Meaning |
|---|---|
| Used websockets | Clients currently connected (including those still completing the handshake). |
| Available websockets | Remaining room before new connections are refused with HTTP 503. |
| Total websockets | The configured $WebSocketMaxClients ceiling. |
Capacity event log
The relay writes capacity and connection-availability events to
/var/log/websocket_stationdata_events/<YYYY-MM-DD>.log, one file per day, so an operator can see
when the relay approached or hit its limits without polling /status. Each line is
[timestamp] [event-type] message. Events:
| Event type | Fires when |
|---|---|
capacity-total-90 | Total connected clients reach 90% of $WebSocketMaxClients. |
capacity-total-100 | Total connected clients reach 100% of $WebSocketMaxClients. |
capacity-ip-90 | A single IP's connections reach 90% of $WebSocketMaxClientsPerIP. |
capacity-ip-100 | A single IP's connections reach 100% of $WebSocketMaxClientsPerIP. |
capacity-total-full | A new connection is refused because the relay is at capacity. |
capacity-total-available | The relay can accept new connections again after having been full. |
log-throttled | The event log itself is rate-limited: at most 10 entries per 5 seconds. Once exceeded, logging stops for 30 seconds and this entry records that it happened; regular events during the hold-off are dropped, not queued. |
Each threshold event fires once per crossing and resets once usage falls back under it, so a station going
90% → 100% → 80% → 95% logs capacity-total-90, capacity-total-100, then
capacity-total-90 again — not a line per connection.