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
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>
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
| Option | Default | Description |
|---|---|---|
key | — | Required. Publishable pk_ key. |
publicationId | — | Scopes 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. |
storage | true | Persist the session in first-party localStorage. |
revalidate | true | Re-sync a restored session once in the background; heals players whose playlist was retired. |
debounce | 250 | Milliseconds to coalesce bursts of set* calls. |
timeout | 15000 | Per-request timeout in milliseconds. 0 disables. |
Methods
| Method | Description |
|---|---|
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:updated—detail: { state, data }everlit:playlist:error—detail: { 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
200means the playlist resolved successfully; any failure surfaces throughonError/ the rejected promise. nameis derived from the tag labels (category:sports→ “Sports”).embed_codeis 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
| Control | Behavior |
|---|---|
| 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. |