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.

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

Eight station events are pushed. Six carry track data, two carry words:

EventSent whenCarries full track data?
now-playingA new song starts playing on a station, or a live override begins.Yes (except live overrides)
next-upA new “next song” appears for a station.Yes
song-requestA listener request is accepted on a station.Yes
song-ratingA star rating is set for a track.Yes
now-playing-on-playlistThe 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
station-eventThe station has something to say to its listeners: a message meant to be shown on websites.No — severity, source, subject and text.
system-eventA 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:

AttributeDescriptionExample
EventMessage 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
StationIDStation 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
StationNameStation name in plain text. Empty for control messages and whenever StationID is 0.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

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 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 WebStreamingOffset before flipping the display, exactly as with the REST endpoint.1785440425
WebStreamingOffsetSeconds to delay the display by to compensate for stream buffering. Always present, even when there is no Track object (e.g. live overrides).12
TrackFull 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" } ]
    }
  }
}
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 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 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…
WebStreamingOffsetSeconds to delay the display by to compensate for stream buffering. Same meaning as on now-playing.12
TrackFull track object, same shape as for now-playing. Omitted if the lookup failed.Object
The relay filters 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 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 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 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 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 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": "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"
  }
}
The starting value is already included in the 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.
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 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 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 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:

EventForA website for listeners…Example
station-eventListeners 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-eventAdministrators and their tools: dashboards, bots, monitoring.ignores it. It must not appear there.Playout restarted.
Both are public. This socket has no login, so a 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 attributeDescriptionDefaultExample
SeverityOne of information, low, medium, high, critical — least to most serious. Always one of these five, in lower case.informationmedium
SourceWho 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.LeisaWolfeLeisaWolfe
SubjectA short subject line: one line, at most 120 characters. Text, escaped.Station message / System messageMaintenance tonight
MessageThe message itself: at most 4096 characters (before escaping), line breaks as \n. May be empty. Text, escaped.emptyThe 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 &amp; &lt; &gt; &quot; &#039;, 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.

DoDo 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 "&amp;lt;" - somebody typed "&lt;" - comes out as "&lt;" 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
Do not run these fields through the decoding used for track titles elsewhere on this feed (entity-decode, then percent-decode, + 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.

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 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] }
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 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

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 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 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 — as station-event and system-event were.
MessagesShow 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 filterIf you filter incoming events by StationID, let 0 through: it marks control messages and messages addressed to the whole network.

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

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
FieldMeaning
Used websocketsClients currently connected (including those still completing the handshake).
Available websocketsRemaining room before new connections are refused with HTTP 503.
Total websocketsThe 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 typeFires when
capacity-total-90Total connected clients reach 90% of $WebSocketMaxClients.
capacity-total-100Total connected clients reach 100% of $WebSocketMaxClients.
capacity-ip-90A single IP's connections reach 90% of $WebSocketMaxClientsPerIP.
capacity-ip-100A single IP's connections reach 100% of $WebSocketMaxClientsPerIP.
capacity-total-fullA new connection is refused because the relay is at capacity.
capacity-total-availableThe relay can accept new connections again after having been full.
log-throttledThe 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.