Repository navigation
999 FM
999 FM is the Juice WRLD API radio — a single endpoint that returns a random, fully playable song on every request. Named after Juice WRLD's 999 brand, it's designed for discover or continuous playback features: call it, stream the song, call it again for the next one.
No authentication required. Returns a full song object plus the direct stream path.
Pick a random song from the full catalogue and return its stream path and metadata.
GET https://juicewrldapi.com/juicewrld/radio/random/
No parameters required. Every call returns a different song.
Response:
{
"id": "Compilation/2. Unreleased Discography/8. WOD (Sessions)/Maze.mp3",
"title": "Maze",
"path": "Compilation/2. Unreleased Discography/8. WOD (Sessions)/Maze.mp3",
"size": 7381244,
"modified": "2025-10-18T19:19:53.784271",
"hash": "d199a85e510b32b9ef3c02a29044a41d",
"song": {
"id": 95001,
"public_id": 2232,
"name": "Maze",
"category": "unreleased",
"era": { "id": 109, "name": "WOD", "description": "WRLD On Drugs era" },
"path": "Compilation/2. Unreleased Discography/8. WOD (Sessions)/Maze.mp3",
"credited_artists": "Juice WRLD",
"length": "2:24",
"lyrics": "...",
"synced_lyrics": "...",
"image_url": "/assets/wod.jpg"
// ... all standard song fields
}
}| Field | Type | Description |
|---|---|---|
id |
string | Internal file path (same as path) |
title |
string | Song title |
path |
string | Stream path — pass to /files/download/?path= |
size |
number | File size in bytes |
modified |
string | ISO 8601 last-modified timestamp |
hash |
string | MD5 file hash (use for deduplication or cache-busting) |
song |
object | Full song object — same shape as GET /songs/{id}/ |
The path field maps directly to the /files/download/ endpoint. Pass it as the stream URL for your audio element.
const BASE = 'https://juicewrldapi.com/juicewrld';
async function getRadioSong() {
const res = await fetch(`${BASE}/radio/random/`);
const data = await res.json();
return {
title: data.title,
streamUrl: `${BASE}/files/download/?path=${encodeURIComponent(data.path)}`,
coverUrl: `https://juicewrldapi.com${data.song.image_url}`,
artist: data.song.credited_artists,
era: data.song.era?.name,
lyrics: data.song.lyrics || null,
syncedLyrics: data.song.synced_lyrics || null,
};
}
// Basic usage
const track = await getRadioSong();
audioElement.src = track.streamUrl;
audioElement.play();import { useState, useCallback } from 'react';
const BASE = 'https://juicewrldapi.com/juicewrld';
export function useRadio() {
const [track, setTrack] = useState(null);
const [loading, setLoading] = useState(false);
const next = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`${BASE}/radio/random/`);
const data = await res.json();
setTrack({
title: data.title,
streamUrl: `${BASE}/files/download/?path=${encodeURIComponent(data.path)}`,
coverUrl: `https://juicewrldapi.com${data.song.image_url}`,
artist: data.song.credited_artists,
era: data.song.era?.name,
length: data.song.length,
lyrics: data.song.lyrics || null,
syncedLyrics: data.song.synced_lyrics || null,
song: data.song,
});
} finally {
setLoading(false);
}
}, []);
return { track, loading, next };
}
// In your component:
function RadioPlayer() {
const { track, loading, next } = useRadio();
return (
<div>
{track ? (
<>
<img src={track.coverUrl} alt={track.title} />
<p>{track.title} — {track.artist}</p>
<audio
src={track.streamUrl}
autoPlay
onEnded={next}
/>
</>
) : (
<button onClick={next} disabled={loading}>
{loading ? 'Loading...' : 'Start 999 FM'}
</button>
)}
<button onClick={next} disabled={loading}>Next</button>
</div>
);
}Separate from /radio/random/ above. That endpoint hands each client its own random song; this is a single shared station — every listener hears the same audio at the same time, with listener counts and community skip/queue votes. No auth required.
| Transport | Endpoint | Purpose |
|---|---|---|
| REST | GET /radio/live/ | One-shot snapshot of station state — now playing, up next, vote, listener counts |
| WebSocket | /ws/radio/ | Live metadata pushes, vote participation, and (optionally) the audio itself |
| HTTP | GET /radio/stream.mp3 | Plain MP3 stream — the fallback when MediaSource is unavailable |
The websocket URL is the API base with its scheme swapped to ws/wss and /ws/radio/ appended — e.g. wss://juicewrldapi.com/juicewrld/ws/radio/.
{
"is_live": true,
"station": "999 FM",
"state": "playing",
"stream_url": "https://juicewrldapi.com/juicewrld/radio/stream.mp3",
"now_playing": {
"title": "Maze",
"artist": "Juice WRLD",
"album": "...",
"display": "Juice WRLD — Maze",
"elapsed_ms": 41000,
"duration_ms": 144000,
"image_url": "/assets/wod.jpg",
"song_id": 95001
},
"up_next": { /* same shape, or null */ },
"queue_preview": ["Song A", "Song B"],
"dj_enabled": true,
"dj_line": "Coming up next…",
"vote": {
"active": true,
"kind": "skip", // "skip" | "queue"
"yes": 3,
"no": 1,
"votes_needed": 5,
"total_listeners": 12,
"seconds_left": 20,
"track": "Maze"
},
"web_listeners": 8,
"discord_listeners": 4,
"total_listeners": 12,
"stale_seconds": null
}song_id on a track is the numeric API song id, so a live track can be looked up via /songs/{id}/ for full metadata, lyrics, or cover art. image_url is relative — prepend https://juicewrldapi.com. vote.active: false means no vote is running and the other vote fields may be absent.
The pool of tracks 999 FM draws from, grouped by era. Useful for building a "what's in rotation" view without polling /radio/live/.
{
"eras": [
{
"name": "WOD",
"tracks": [
{ "id": "94086", "title": "Maze", "artist": "Juice WRLD" }
]
}
]
}The socket carries both metadata and audio: text frames are JSON station-state objects (same shape as /radio/live/), binary frames are MP3 chunks. Set binaryType = 'arraybuffer' and branch on the frame type.
const ws = new WebSocket('wss://juicewrldapi.com/juicewrld/ws/radio/')
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) => {
if (typeof e.data === 'string') {
const state = JSON.parse(e.data) // RadioLiveState — update the UI
} else {
// MP3 chunk — feed to a MediaSource SourceBuffer('audio/mpeg')
}
}Client → server messages:
| Message | Purpose |
|---|---|
{ type: "listening", value, audio } |
Join/leave the listener count. audio is "ws" or "http" depending on which stream you're consuming. Re-send on reconnect if still listening. |
{ type: "propose_skip" } |
Start a vote to skip the current track |
{ type: "propose_queue", song_id } |
Start a vote to queue a song. song_id must be the number — a stringified id is silently ignored and the vote never starts. |
{ type: "vote", value } |
"yes" or "no" on the active vote |
Playback notes:
- Binary frames only make sense with MediaSource (
audio/mpeg). Where it's unsupported, ignore them and point an<audio>element at/radio/stream.mp3instead — tell the server which you chose via theaudiofield. - Because it's a live stream, buffered audio drifts behind. Seek forward when you fall more than a few seconds behind the buffered end, and evict old buffered ranges or the SourceBuffer eventually throws
QuotaExceededError. - Reconnect on close — background tabs get their socket closed and audio paused silently, with no event fired, so a periodic health check is worth having.
- Calls are not seeded — every request is independent. Repeats are possible but rare given the 2,452-song catalogue.
- The
songobject is identical to/songs/{id}/— full producers, engineers, lyrics, synced lyrics, and groupbuy info included. -
image_urlis a relative path (e.g./assets/wod.jpg) — prependhttps://juicewrldapi.comfor use in<img>tags. - The stream endpoint supports HTTP Range requests — the browser
<audio>element handles seeking automatically. - No rate limiting on public endpoints, but call once per track end — not on a tight loop.