obboOpen workspace
Menu
Back to the player playground

Integration guide

Add a player to your site

Give visitors a way to listen without leaving your website. Create an audio player in Obbo, paste its code into your page, and you're ready. If someone else manages your website, send them this guide.

The first three steps are all most websites need. Styling and developer reference are optional.

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.
  4. Publish changes and keep the player enabled. Draft edits are not visible to your website visitors.

Open your audio players

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.

New finished episodes in a whole-podcast player appear automatically. If you change the selected episodes or player settings, publish those changes. The embed code does not need replacing.

02Paste two lines

Replace YOUR_WIDGET_ID with your player ID. Put the script on your page and the div where the player should appear. In a website builder, use a custom HTML or embed block that allows scripts.

HTML
<script src="https://www.obbo.me/player/obboplayer.js" async></script>
<div data-obbo-player="YOUR_WIDGET_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.

Audio only downloads after someone presses play.The player loads its episode list and artwork with the page, but requests audio only after a visitor starts playback. A page view does not count as a play.

03Check your published page

Save and publish your website, then open the page as a visitor would. Some website editors do not run scripts inside their editing preview.

  • Your podcast title and finished episodes should appear without signing in to Obbo.
  • Press play, try the seek bar, and pause. Audio should only start after you ask for it.
  • Check the page on a phone as well as a wider screen. If the player is missing, make sure your website editor allows custom HTML and external scripts.

You're ready.You can stop here. Explore the sections below only if you want another layout, your own branding, or a custom integration.

04Pick a layout

There are six visible layouts, plus auto. Choose one with data-obbo-ui. Omit it or use auto to follow your player's published compact or playlist setting.

Back to the player playground

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.
HTML
<script src="https://www.obbo.me/player/obboplayer.js" async></script>
<div
  data-obbo-player="YOUR_WIDGET_ID"
  data-obbo-ui="list"
  data-obbo-theme="dark"
></div>

Every attribute

Setting or identifierWhat it does
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-languageAudio language: en, es, da, ja, de, fr, it, nl, sv, no or pl. Defaults to the page's lang attribute. An episode without that language plays its original recording.
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.

05Make it look like your site

The player renders into your page, not an iframe. Use the variables below to match your colors, corner radius and font. The example includes the class that connects your HTML to your CSS.

The quick way: nine variables

CSS variableDefault value
--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 font stack
HTML / CSS
<div class="my-player" data-obbo-player="YOUR_WIDGET_ID"></div>

<style>
  .my-player {
    --obbo-player-accent: #a1482e;
    --obbo-player-radius: 8px;
    --obbo-player-font: system-ui, sans-serif;
  }
</style>

The thorough way: class names

Stable obbo- classes let your stylesheet target the controls. Prefer variables for branding; use classes for more detailed changes. The root's data-obbo-state attribute lets you style loading and error states. Keep error messages visible so visitors know when playback is unavailable.

Setting or identifierWhat it does
.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.
CSS
.obbo-p[data-obbo-state="loading"] { opacity: 0.6; }
.obbo-p[data-obbo-state="error"] .obbo-note { font-weight: 700; }

If your CSS is aggressive, seal it off.Add data-obbo-isolate to an in-page layout to protect it from your site's CSS. The nine variables still pass through, but class-level styling no longer reaches inside. The dock layout attaches to the document and does not support shadow isolation.

06Build 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.

Live example from Momento. These custom controls use the same playback engine as the ready-made layouts.

0:00

Loading the sample…

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

<button id="play" type="button" disabled>Play</button>
<span id="time">0:00</span>

<script>
  addEventListener('load', function () {
    var player = Obbo.createPlayer({ widgetId: 'YOUR_WIDGET_ID' });
    var button = document.getElementById('play');
    var time = document.getElementById('time');

    player.subscribe(function (state) {
      button.disabled = state.status !== 'ready' || !state.playable;
      button.textContent = state.playing ? 'Pause' : 'Play';
      time.textContent = Obbo.formatTime(state.position);
    });

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

07The 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 transcript is selectable text that assistive technology can read once loaded. Keep essential written content in your page's own HTML when search visibility matters; JavaScript-rendered text is not guaranteed to be indexed.
HTML
<script src="https://www.obbo.me/player/obboplayer.js" async></script>
<div data-obbo-player="YOUR_WIDGET_ID" data-obbo-ui="transcript"></div>

Uses the transcript available for each episode.Finished transcripts are served by the embed API. If an episode has no transcript, playback still works and the player explains that no transcript is available. The playground includes a working read-along example.

08API reference

Obbo.createPlayer(options)

Setting or identifierWhat it does
widgetIdRequired. Your player ID.
localeLanguage for built-in labels. Defaults to en.
languageAudio language to request. Set this to your page's language; episodes without that language use the original recording.
apiBasePublic API origin. Defaults to https://app.obbo.me. Only change it when using a different Obbo server.
autoAdvanceContinue to the next episode. Off by default.
exclusivePause other players on the page when this one starts. On by default.

The player

Setting or identifierWhat it does
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

Setting or identifierWhat it does
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

Setting or identifierWhat it does
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.

09React, 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.

React
import { useEffect, useRef, useState } from 'react';

let sdkPromise;
function loadPlayer() {
  if (window.Obbo) return Promise.resolve(window.Obbo);
  if (sdkPromise) return sdkPromise;
  sdkPromise = new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://www.obbo.me/player/obboplayer.js';
    script.async = true;
    script.onload = () => {
      if (window.Obbo) resolve(window.Obbo);
      else {
        sdkPromise = null;
        script.remove();
        reject(new Error('Player SDK unavailable'));
      }
    };
    script.onerror = () => {
      sdkPromise = null;
      script.remove();
      reject(new Error('Player SDK could not load'));
    };
    document.head.appendChild(script);
  });
  return sdkPromise;
}

export function Player({ widgetId, locale = 'en' }) {
  const host = useRef(null);
  const [failed, setFailed] = useState(false);
  const [attempt, setAttempt] = useState(0);

  useEffect(() => {
    let cancelled = false;
    let cleanup;
    loadPlayer().then((Obbo) => {
      if (cancelled) return;
      const player = Obbo.createPlayer({ widgetId, locale, language: locale });
      const ui = Obbo.ui.list(host.current, player, { locale });
      cleanup = () => { ui.destroy(); player.destroy(); };
    }).catch((error) => {
      if (!cancelled) {
        console.error('Player failed to load', error);
        setFailed(true);
      }
    });
    return () => { cancelled = true; cleanup?.(); };
  }, [widgetId, locale, attempt]);

  return <>
    <div ref={host} />
    {failed && <p role="alert">
      The player could not load.
      <button type="button" onClick={() => {
        setFailed(false);
        setAttempt((value) => value + 1);
      }}>Try again</button>
    </p>}
  </>;
}

This example waits for the SDK, cancels late mounts, and destroys both the layout and player on cleanup. Pass your website's language as locale. Apply the same lifecycle to Vue's onMounted/onUnmounted or Svelte's onMount. Inline examples may need a CSP nonce or an external JavaScript file under your site's policy.

10Content-Security-Policy

If your site sends a Content-Security-Policy header, review these directives with your developer. Add the required hosts to your existing policy; these rows are not a complete policy to paste.

Setting or identifierWhat it does
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.
style-srcThe SDK injects an inline stylesheet. Your policy must allow it; the current bundle does not provide a stylesheet nonce. Review this requirement with your developer rather than disabling CSP.
img-srcAllow your podcast artwork host, commonly S3. Use your actual artwork URL to determine the narrowest host allowlist.

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.

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

12When 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

Back to the player playground
Add a podcast player to your site