Build your own player with the widget API

Prefer full control over the look and feel? The same widget that powers the embed player is available as a small, token-based API — just your tracks, your colors, your UI.

Our last post walked through Muzlo widgets — a curated playlist plus an embeddable player you can drop onto any site with one <iframe>. That covers most people just fine. But some of you are developers, and what you actually want is not our player — it is our data, so you can build your own.

That is exactly what the widget API is for.

The idea

Every widget you create already has a private token attached to it — that is what makes the embed link work. The same token also unlocks a small, read-only API: a JSON endpoint describing the widget (its name and colors) plus the list of tracks in it, each with a ready-to-use streaming URL.

Nothing here needs a login, a session, or an API key separate from the widget itself. If you can see the widget in your Muzlo account, you already have everything you need to call the API for it.

And just like the embed player, the API only ever exposes the specific tracks you added to that widget — never your whole library.

What you get back

One request to the widget's public endpoint returns everything needed to build a player:

{
  "name": "My widget",
  "accentColor": "#6366f1",
  "backgroundColor": "#ffffff",
  "textColor": "#0f0f0f",
  "activeColor": "#6366f1",
  "dividerColor": "#e5e7eb",
  "tracks": [
    {
      "id": "a1b2c3",
      "title": "Track title",
      "artist": "Artist name",
      "durationSeconds": 214,
      "coverUrl": "https://muzlo.online/api/files/a1b2c3/cover?widget=...",
      "streamUrl": "https://muzlo.online/api/files/a1b2c3/download?widget=..."
    }
  ]
}

The colors are there in case you want your custom player to inherit the same theme you already picked in the dashboard — handy if you want a consistent look between the default embed and your own version elsewhere. activeColor is what highlights the track that's currently playing, and dividerColor is the color of the lines separating tracks in the list — both are set independently from accentColor, so you can tune them without touching the rest of the palette.

There are actually two independent switches in the dashboard for a widget. One turns off the token entirely — this endpoint 404s, and so do the file links, so nothing works until you flip it back on. The other only disables Muzlo's own pre-built /embed player page, and doesn't affect this API at all — it's there for people who want to ship only their own custom player and keep the default embed page dark, without losing API access.

streamUrl points straight at the audio file and supports range requests, so seeking and progressive loading work the way you'd expect from any normal <audio> tag. coverUrl works the same way for artwork.

A minimal example

const res = await fetch(`https://muzlo.online/api/public/widgets/${token}`)
const { name, tracks } = await res.json()

const audio = new Audio(tracks[0].streamUrl)
audio.play()

That's genuinely the whole integration for a basic player. From there it's just your UI: a track list, a play button, a progress bar — however you want it to look.

Because the endpoint is open to cross-origin requests, you can call it directly from your site's own frontend code — no backend proxy required.

What it's good for

  • A fully custom player that matches your site's design system pixel-for-pixel, rather than living in an iframe.
  • Pulling track metadata into a page — a "now playing" widget, a track listing, a release page — without embedding a player at all.
  • Prototyping something quick without wiring up authentication.

What it isn't

It's deliberately narrow: read-only, and scoped to one widget's track list. You manage what's in the widget — adding tracks, reordering, recoloring — back in your Muzlo account, not through this API. That keeps the public surface small and predictable, and your library safe regardless of where a token ends up.

If a token ever gets shared somewhere you didn't intend, regenerating it from the widget's settings page kills the old one instantly.