On-Demand Playlists

Let your audience build their own audio playlists

A small, dependency-free JavaScript SDK that turns any tag picker on your site into a live audio playlist builder. Visitors choose topics, Everlit assembles the matching stories into a playlist, and the player appears inline — no accounts, no backend work on your side.

Overview

everlitPlaylist.js connects your page to the Everlit On-Demand endpoint using a browser-safe publishable key (pk_…). Each combination of tags and options resolves to one canonical playlist, shared by every listener who builds the same combination.

1. Pick Tags

Visitors choose topics with your UI; typeahead runs client-side against a cached tag manifest

2. Resolve

Identical selections resolve to one canonical playlist — created once, shared by everyone

3. Listen

The Everlit player mounts inline and updates live as the selection changes

4. Save & Share

Every response includes a ready-to-store embed code that keeps playing fresh matching stories

Playlists are immutable. A playlist's rules never change after creation — changing tags simply resolves to a different playlist. An embed code your users save today will play the same kind of content forever, refreshed as new stories publish.

Quick Start

Load the SDK, give it a container and your publishable key, and wire it to your tag UI:

<script src="https://cdn.everlit.audio/libs/everlitPlaylist.js"></script>
<div id="player"></div>
<script>
  var pl = Everlit.playlist('#player', {
    key: 'pk_live_xxx',            // your publishable key
    publicationId: 'pblc_xxx'      // enables the tag manifest + typeahead
  });

  // Wire to your own tag UI. Rapid changes are debounced + serialized.
  myTagPicker.on('change', function (tags) { pl.setTags(tags); });

  // Optional refinements:
  pl.setOptions({ size: 10, order: 'latest', match: 'any' });
</script>
Publishable keys are browser-safe. A pk_ key can only create On-Demand playlists and read your tag manifest — never the management API. Contact support@everlit.audio to get one for your publication, optionally pinned to your domains.

Tag Typeahead

The SDK fetches your publication's tag manifest once (cached in memory and localStorage) and searches it entirely client-side — no network requests per keystroke.

picker.on('focus',  function ()  { pl.loadTags(); });                  // lazy-load on first focus
picker.on('input',  function (q) { render(pl.searchTags(q, { limit: 8 })); });
picker.on('select', function (entries) { pl.setTags(entries); });       // entries carry opaque values

Each manifest entry is { label, value, namespace }. Pass entries straight to setTags() — the SDK sends each entry's opaque value, so you never deal with tag-value conventions yourself.

API Reference

Options

OptionDefaultDescription
keyRequired. Publishable pk_ key.
publicationIdScopes the playlist and tag manifest to one publication. Required for loadTags().
height"180px"Player iframe height.
onUpdate(state, data)Called after each successful sync.
onError(err)Called on failure. err.status, err.retryAfter (on 429), err.timeout.
storagetruePersist the session in first-party localStorage.
revalidatetrueRe-sync a restored session once in the background; heals players whose playlist was retired.
debounce250Milliseconds to coalesce bursts of set* calls.
timeout15000Per-request timeout in milliseconds. 0 disables.

Methods

MethodDescription
setTags(tags)Set the tag selection (manifest entries or raw value strings). Returns a Promise of the API response.
setOptions({ match, size, order, within, … })Adjust playlist options.
set({ tags, …options })Both at once.
loadTags([force])Fetch + cache the tag manifest. Lazy; call on first picker focus.
searchTags(query, { limit, namespace })Synchronous typeahead over the loaded manifest.
setTagsByLabel(labels)Resolve display labels to values, then sync.
snapshot()Current { id, tags, options } without touching the network.
reset()Cancel pending work, clear storage, remove the player.

Events

Dispatched on document (bubbling), as alternatives to the callbacks:

  • everlit:playlist:updateddetail: { state, data }
  • everlit:playlist:errordetail: { error, reason, status, retryAfter }

Response Shape

Every successful sync resolves with the playlist as the API serializes it:

{
  "id": "plist_8fk2m1qz04x",
  "type": "Playlist",
  "name": "Sports, Colorado",
  "on_demand": true,
  "tags": ["category:sports", "colorado"],
  "match": "any",
  "size": 10,
  "order": "latest",
  "within": "any",
  "publication_ids": ["pblc_9dh31vq7k2m"],
  "embed_url": "https://app.everlit.audio/embeds/plist_8fk2m1qz04x",
  "embed_code": "<iframe src=\"...\" ...></iframe>"
}
  • A 200 means the playlist resolved successfully; any failure surfaces through onError / the rejected promise.
  • name is derived from the tag labels (category:sports → “Sports”).
  • embed_code is a ready-to-store iframe snippet — ideal for a “save this playlist” feature in your CMS or app.

Complete Example

A self-contained page with a working typeahead tag picker, playlist options, and a “save this playlist” hook. Swap in your pk_ key and publication ID. Prefer to click around first? Open the live demo.

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Build your playlist</title></head>
<body>
  <h2>Build your own audio playlist</h2>

  <input id="tag-search" placeholder="Search topics…" autocomplete="off">
  <ul id="suggestions"></ul>
  <div id="picked"></div>

  <label>Stories: <select id="size"><option>5</option><option selected>10</option><option>25</option></select></label>
  <button id="save" hidden>Save this playlist</button>

  <div id="player"></div>

  <script src="https://cdn.everlit.audio/libs/everlitPlaylist.js"></script>
  <script>
    var pl = Everlit.playlist("#player", {
      key: "pk_live_XXXXXXXXXXXX",          // your publishable key
      publicationId: "pblc_XXXXXXXXXXX",    // enables the tag manifest
      height: "380px",
      onUpdate: function (state, data) {
        // data.embed_code is a ready-to-store iframe snippet for a CMS
        // "saved playlists" feature.
        document.getElementById("save").hidden = false;
        document.getElementById("save").onclick = function () {
          myCms.savePlaylist({ title: data.name, embedCode: data.embed_code });
        };
      },
      onError: function (err) {
        if (err.status === 429) console.warn("rate limited, retry in", err.retryAfter, "s");
      }
    });

    var picked = [];   // manifest entries the visitor selected
    var search = document.getElementById("tag-search");
    var list = document.getElementById("suggestions");

    // Lazy-load the tag manifest on first focus; typeahead is then fully
    // client-side — no network per keystroke.
    search.addEventListener("focus", function () { pl.loadTags(); });
    search.addEventListener("input", function () {
      var hits = pl.searchTags(search.value, { limit: 8 });
      list.innerHTML = "";
      hits.forEach(function (entry) {
        var li = document.createElement("li");
        li.textContent = entry.label + (entry.namespace ? " (" + entry.namespace + ")" : "");
        li.onclick = function () {
          picked.push(entry);          // entries carry the opaque value the API expects
          search.value = ""; list.innerHTML = "";
          renderPicked();
          pl.setTags(picked);          // debounced + serialized; resolves with the response
        };
        list.appendChild(li);
      });
    });

    document.getElementById("size").addEventListener("change", function (e) {
      pl.setOptions({ size: parseInt(e.target.value, 10) });
    });

    function renderPicked() {
      var el = document.getElementById("picked");
      el.innerHTML = "";
      picked.forEach(function (entry, i) {
        var b = document.createElement("button");
        b.textContent = entry.label + " ✕";
        b.onclick = function () { picked.splice(i, 1); renderPicked(); pl.setTags(picked); };
        el.appendChild(b);
      });
    }
  </script>
</body>
</html>

What the visitor experiences: they pick “Sports” and “Colorado”, a player for the canonical Sports, Colorado playlist appears, changing the story count swaps in that combination's playlist, a page reload restores the session from localStorage, and a saved embed_code keeps playing fresh matching stories indefinitely.

Immutable Playlists

Every playlist is keyed by a fingerprint of its rules — tags, match mode, size, order, and date window. Identical rule-sets always resolve to the same canonical playlist.

Because playlists are immutable, changing your selection never edits a playlist someone else might be listening to — it simply resolves to a different one, and the SDK re-points the player. Bursts of rapid changes are debounced into a single request, and at most one request is ever in flight, so responses can't apply out of order.

The tracks inside a playlist stay fresh: its rules are frozen, but Everlit keeps resolving them against your newest published content. Playlists that nobody builds, plays, or even views for ~90 days are retired automatically; a retired combination is simply re-created the next time someone asks for it (the SDK's restored sessions revalidate on load, so returning visitors never see a dead player).

Keys, Limits & Safety

ControlBehavior
Publishable keys pk_ keys can only create On-Demand playlists and read the tag manifest. They cannot touch the management API, and only real tags from your enabled namespaces are accepted.
Domain allowlist Keys can be pinned to your site's domains; requests from other origins are rejected.
Rate limiting Per-key, per-visitor limits. On HTTP 429 the error carries retryAfter (seconds); the SDK surfaces it via onError and the error event.
Field allowlist Only playlist rule fields are accepted from the browser — names and titles are derived server-side from your tags, so visitors can't inject arbitrary text.
Listener identity The SDK maintains the same anonymous first-party eut identifier as the Auto Audio widget (one shared cookie per browser) and passes it to the player, so playlist listening unifies with the rest of your Everlit analytics. No personal data is collected.

Next Steps