Skip to content
leanwrldd edited this page Sep 10, 2026 · 2 revisions

← Back to Home

999 FM

What is 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.

GET /radio/random/

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

Streaming the result

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();

React hook — useRadio

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>
  );
}

Live Radio — one shared broadcast

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/.

GET /radio/live/ — Station State

{
  "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.

GET /radio/library/ — Track Library

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

WebSocket /ws/radio/

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.mp3 instead — tell the server which you chose via the audio field.
  • 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.

Notes

  • Calls are not seeded — every request is independent. Repeats are possible but rare given the 2,452-song catalogue.
  • The song object is identical to /songs/{id}/ — full producers, engineers, lyrics, synced lyrics, and groupbuy info included.
  • image_url is a relative path (e.g. /assets/wod.jpg) — prepend https://juicewrldapi.com for 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.

Clone this wiki locally