Integration guide

Add a player to your site

One script tag gives you a working player. The same script gives you a JavaScript API, so if none of the ready-made layouts suits your design you can build your own and still get playback, episode data and listening stats. No build step, no framework, no npm package.

01Get a player ID

A player is the thing you embed. It points at a whole podcast or at specific episodes, and it has a public ID you paste into your HTML.

  1. Open Account, then Audio players.
  2. Create a player, name it for your own reference, and add what it should play: a whole podcast, one episode, or several episodes in the order you choose.
  3. Copy the player ID.

The ID is meant to be public.It sits in your page source where anyone can read it, and that is fine. It grants one thing: permission to play the episodes you put in that player. It is not an API key, it reaches nothing else in your account, and deactivating the player revokes it everywhere at once.

Adding an episode later makes it appear in every embed of that player straight away. Nothing needs re-pasting.

02Paste two lines

Put the script anywhere on the page, and the div where you want the player.

<script src="https://www.obbo.me/player/obboplayer.js" async></script>

<div data-obbo-player="YOUR_PLAYER_ID"></div>

That is a complete integration. The script finds every element with a data-obbo-player attribute and fills it in, whether the page has one player or ten. Loading the script twice is harmless.

Nothing downloads until someone presses play.The player fetches its episode list on load, but no audio is requested until a visitor actually starts one. Putting a player on a page costs your visitors nothing and never registers a play that did not happen.

03Pick a layout

Seven are built in. Choose one with data-obbo-ui; leave it off and you get whichever you selected when you made the player, so you can change your mind later without editing any pages.

ValueWhat it is
listThe controls with your episode list underneath. For a whole podcast, or when someone might want an episode other than the newest.
barJust the controls. For an article or product page built around one episode.
cardCover art at full size with the button on its edge. For sidebars and grids, where a horizontal bar looks stapled on.
inlineA play button and a clock, sized to sit inside a sentence rather than interrupt it.
dockPins to the bottom of the window once someone presses play, so the controls do not scroll away. Dismissible.
transcriptThe episode as readable text that follows the audio. The spoken line is highlighted; click any line to jump there.
autoThe default. Follows the layout saved with the player.
<div data-obbo-player="YOUR_PLAYER_ID"
     data-obbo-ui="list"
     data-obbo-theme="dark"></div>

Every attribute

data-obbo-playerRequired. Your player ID.
data-obbo-uilist, bar, card, inline, dock, transcript, or auto.
data-obbo-themelight or dark.
data-obbo-episodeStart on a particular episode. It is selected, not played.
data-obbo-isolateRender inside a shadow root, sealed off from your CSS.
data-obbo-localeLanguage for the player's own buttons: en, es, da or ja. Episode titles come from your podcast either way.
data-obbo-languageWhich language of the audio to play: en, es, da or ja. Defaults to your page's own lang, and plays the original when an episode has no render in that language.
data-obbo-autoadvancePlay the next episode when one ends.

Several players on one page is fine.They share a single request for your episode list, and starting one pauses the others, so a visitor never ends up with two voices at once.

04Make it look like your site

The player renders into your page's own DOM, not a sealed frame. Your stylesheet reaches it and your fonts are inherited.

The quick way: nine variables

--obbo-player-bg#ffffff
--obbo-player-fg#1c2733
--obbo-player-muted#66768a
--obbo-player-accent#2f6fed
--obbo-player-accent-fg#ffffff
--obbo-player-border#e3e8ef
--obbo-player-row-hover#f4f6f9
--obbo-player-radius12px
--obbo-player-fontsystem stack
.my-player {
  --obbo-player-accent: #e5484d;
  --obbo-player-radius: 4px;
  --obbo-player-font: 'Söhne', system-ui, sans-serif;
}

@media (prefers-color-scheme: dark) {
  .my-player {
    --obbo-player-bg: #181d24;
    --obbo-player-fg: #f2f5f8;
  }
}

The thorough way: class names

Every element carries a stable obbo- class, and every rule we ship is a single class selector, so your own rule wins without needing !important. The root also carries data-obbo-state, which lets you style the loading and error phases without reading our internals.

.obbo-pThe player root. Carries data-obbo-state.
.obbo-playPlay and pause button.
.obbo-seekThe scrubber, an input of type range.
.obbo-title / .obbo-subEpisode title and podcast name.
.obbo-artCover image. Hidden when the podcast has none.
.obbo-list / .obbo-rowEpisode list and its rows. The playing row carries aria-current.
.obbo-noteThe loading, empty and error message.
.obbo-p[data-obbo-state="loading"] { opacity: 0.6; }
.obbo-p[data-obbo-state="error"]   { display: none; }

If your CSS is aggressive, seal it off.A heavy framework reset can reach into the player. Add data-obbo-isolate and it renders in a shadow root instead: immune to your stylesheet, though the nine variables still pass through, so you keep the colour controls and lose only the class-level ones.

05Build your own player

Skip the layouts entirely. createPlayer gives you episode data, playback and position; you write the markup. Listening still gets counted and expired audio links still recover, without either being your problem. The controls below are running the code underneath them.

<button id="play" disabled>Play</button>
<span id="title"></span>
<div id="track"><div id="fill"></div></div>

<script>
  var player = Obbo.createPlayer({ widgetId: 'YOUR_PLAYER_ID' });

  player.subscribe(function (state) {
    play.disabled     = state.status !== 'ready';
    play.textContent  = state.playing ? 'Pause' : 'Play';
    title.textContent = state.episode ? state.episode.title : '';
    fill.style.width  = (state.position / state.duration * 100) + '%';
  });

  play.onclick = function () { player.toggle(); };
</script>

Keep the button disabled until status is ready.On iOS, permission to play audio is tied to the tap that asked for it. A tap the player cannot act on yet is spent for nothing, and your visitor has to tap again.

06The follow-along transcript

The transcript layout puts the whole episode on your page as text, and keeps it in step with the audio. Every Obbo episode is transcribed with sentence-level timings as part of production, so there is nothing for you to prepare.

  • The line being spoken is highlighted and scrolls itself into view. Scroll it yourself and it stops following, with a button to jump back — it will not fight you for the scrollbar.
  • Clicking any line plays from there, which makes the episode quotable: a reader can find the sentence they want and hear it said.
  • The text is real text in your page, so search engines index it and screen readers read it. An episode that would otherwise be an opaque audio box becomes content about your subject.
<div data-obbo-player="YOUR_PLAYER_ID"
     data-obbo-ui="transcript"></div>

Not switched on yet.Transcripts are produced for every episode but are not yet published on the embed API, so this layout currently shows the player with a note in place of the text. It will fill in on its own once we turn the field on — nothing on your page needs to change.

07API reference

Obbo.createPlayer(options)

widgetIdRequired. Your player ID.
localeLanguage for built-in labels. Defaults to en.
autoAdvanceContinue to the next episode. Off by default.
exclusivePause other players on the page when this one starts. On by default.

The player

subscribe(fn)Calls your function with the full state on every change, and once immediately. Returns a function that unsubscribes.
getState()The current state, if you would rather pull than subscribe.
play(episodeId?)Start playing, optionally switching episode first.
pause() / toggle()The obvious things.
select(episodeId)Switch episode without playing. Makes no request.
seek(seconds)Jump to a position, in seconds.
setRate(rate)Playback speed, from 0.75 to 2.
next() / previous()Move through the episode list.
reload()Fetch the episode list again.
destroy()Stop and detach. Call it when your component unmounts.
mediaThe underlying audio element, for anything we have not thought of.

The state

statusloading, ready, empty or error.
errorEmpty, or blocked when the browser refused without a tap, load_failed, or unavailable for a wrong or revoked ID.
episodesEvery episode in the player: title, description, number, date and duration.
episodeThe selected one. Selected does not mean playing.
podcastTitle, company name and cover image.
playingWhether audio is running right now.
position / durationBoth in seconds.
playableWhether the selected episode can be played at all.

Also on Obbo

Obbo.formatTime(s)Turns 65 into 1:05, the same way the built-in layouts do.
Obbo.ui.list(el, player, opts)Mount a built-in layout yourself. Returns an object with destroy.
Obbo.mount(root?)Scan for player elements and mount them. Call it after adding markup dynamically.
Obbo.unmountElement(el)Tear one down.
Obbo.versionWorth quoting in a bug report.

08React, Vue and friends

Do not rely on the automatic scan. A framework that re-renders the container will wipe out the player's markup and leave its event listeners behind. Mount it yourself and tear it down on unmount, into an element you never render into.

function Player({ widgetId }) {
  const host = useRef(null);

  useEffect(() => {
    const player = Obbo.createPlayer({ widgetId });
    const ui = Obbo.ui.list(host.current, player);
    return () => { ui.destroy(); player.destroy(); };
  }, [widgetId]);

  return <div ref={host} />;
}

Load the script once, in your document head or through a small loader, before the effect runs. The same applies to Vue's onMounted and onUnmounted, and to Svelte's onMount.

09Content-Security-Policy

If your site sends a Content-Security-Policy header, it needs three directives.

script-src https://www.obbo.meTo load the player.
connect-src https://app.obbo.meTo fetch your episode list.
media-src https://app.obbo.me https://*.amazonaws.comTo stream the audio itself.

media-src is the one people miss.Audio is served from storage on a different host than the API. Leave it out and the player looks completely healthy, right up until someone presses play.

10What gets counted

Listening on your site shows up in your Obbo analytics: how many times the player loaded, how many plays started, and how much was actually heard.

  • Your visitors are never identified. No account, no login, and the player sets no cookie. Reports are attributed to you as the owner, not to whoever is listening.
  • A page view is not a play. Nothing counts as a play until audio actually starts.
  • Listening time is measured rather than assumed. Skipping ahead does not count as having heard what you skipped.

11When it does not work

Nothing appears at all

Check the element has data-obbo-player with your ID, and that the script tag is on the page. If you added the markup after the page loaded, call Obbo.mount() once afterwards.

It says the player is unavailable

The ID is wrong, or the player has been deactivated in your account. Both give the same message on purpose.

It says there are no episodes

The player is empty, or its episodes are still being produced. Only finished episodes are served.

It looks right but will not play

Almost always media-src missing from your Content-Security-Policy. Otherwise check state.error: blocked means the browser wanted a real tap first, which happens when playback is triggered by something other than a click.

It looks wrong

Your page's CSS is reaching into it, which is usually the point but occasionally is not. Add data-obbo-isolate to seal it off.

Still stuck?Send us the page address, your player ID and the value of Obbo.version at hello@obbo.me

See all the layouts, running

Add a podcast player to your site