Integration

Show recognized music on a Twitch stream

Display the currently-playing song to Twitch viewers with AudD audio streams — subscribe a channel with addStream, consume matches via callback or longpoll, and render them in an overlay or a Twitch Extension.

view .md auddtwitchtwitch extensionnow playing

If you run a music channel on Twitch — a DJ set, a radio rebroadcast, a listening stream — you can show viewers the artist and title of whatever is playing right now. AudD recognizes the audio off the live broadcast and delivers each match to you; you render it as an on-screen overlay or inside a Twitch Extension panel. This page covers both the simplest path you can build yourself and AudD’s official open-source Twitch Extension you can fork.

What you’ll build

The pipeline has three parts that are the same whether you ship an overlay or a full Extension:

  1. A subscribed channel. You register a Twitch channel with AudD once using the twitch:<channel> shortcut. AudD pulls the broadcast audio continuously and fingerprints it against its 160-million-track database.
  2. A match consumer. AudD delivers each recognized song to your backend, either by POSTing a callback to a URL you host or by letting your process longpoll an HTTP endpoint. Your backend keeps the current song per channel.
  3. A frontend. Either an OBS browser-source overlay that the streamer composites into their scene, or a Twitch Extension panel/video-overlay that viewers see natively in the Twitch player. The frontend reads the current song from your backend.

The streams API lives on https://api.audd.io/. Two approaches follow: (a) build it yourself from the streams API, and (b) fork AudD’s official Extension, which already implements the consumer and the frontend.

Audio streams are a paid add-on, billed per stream per month. The public test token does not work on the streams endpoints — set your real token from dashboard.audd.io before any of the calls below.

Approach A: build it yourself

This is the right approach when you want a custom overlay, you’re already running a backend, or you only need an OBS browser source rather than a viewer-facing Twitch Extension.

Prerequisites

  • An API token from dashboard.audd.io with the audio-streams add-on enabled.
  • Node.js 18+ with the official SDK: npm install @audd/sdk
  • The Twitch channel name you want to recognize (the part after twitch.tv/, e.g. monstercat).

Step 1: Set the account callback URL (once)

The callback URL is configured per account, not per stream — every stream on the account POSTs to the same URL. Set it once before adding any channels. Longpoll also requires that a callback URL be set, so do this step even if you intend to longpoll.

import { AudD } from "@audd/sdk";

const audd = new AudD(process.env.AUDD_API_TOKEN!); // dashboard.audd.io

await audd.streams.setCallbackUrl(
  "https://your-app.example.com/audd-callback",
  { returnMetadata: ["apple_music", "spotify"] }, // optional streaming links
);

If you have no public receiver yet and only plan to longpoll, point it at the no-op URL https://audd.tech/empty/, which accepts and discards the POSTs:

await audd.streams.setCallbackUrl("https://audd.tech/empty/");

Step 2: Subscribe the Twitch channel

Register the channel with addStream, passing the twitch:<channel> shortcut as the URL. You pick a radioId — any integer — as your handle for this channel; it comes back on every match so you can tell channels apart.

await audd.streams.add({
  url: "twitch:monstercat",   // the part after twitch.tv/
  radioId: 1,                 // your handle for this channel
  callbacks: "before",        // deliver at song START — best for "now playing"
});

callbacks: "before" delivers the match the moment a song starts playing, which is what a live display needs. The tradeoff is that a “before” callback doesn’t carry the song’s total played time (AudD doesn’t know it yet); a now-playing display doesn’t need that.

To point AudD at a different channel later use audd.streams.setUrl(radioId, "twitch:othername"), list everything on the account with audd.streams.list(), and remove a channel with audd.streams.delete(radioId).

Step 3: Consume matches

Pick one of the two delivery methods. If you can host a public HTTPS endpoint, callbacks are the lowest-latency, lowest-effort path. If you can’t (prototyping locally, behind NAT, or running the consumer in a browser), longpoll over an open HTTP connection instead. The full tradeoff is in Callbacks vs. longpoll.

Callback receiver:

import express from "express";
import { handleCallback } from "@audd/sdk";

const app = express();
app.use(express.json());

// Current song per channel. Use Redis in production.
const nowPlaying = new Map<number, unknown>();

app.post("/audd-callback", async (req, res) => {
  try {
    const { match, notification } = await handleCallback(req);
    if (match) {
      const s = match.song;
      nowPlaying.set(match.radioId, {
        artist: s.artist,
        title: s.title,
        cover: s.songLink ? `${s.songLink}?thumb` : null, // lis.tn cover art
        songLink: s.songLink,
        at: Date.now(),
      });
    } else if (notification) {
      // 650 = can't connect to the stream; 651 = only white noise
      console.warn("stream", notification.notificationCode, notification.notificationMessage);
    }
    res.sendStatus(200); // ALWAYS 200 — see note below
  } catch (err) {
    console.error("bad callback body", err);
    res.sendStatus(400);
  }
});

// The frontend reads this:
app.get("/now-playing/:radioId", (req, res) => {
  res.json(nowPlaying.get(Number(req.params.radioId)) ?? null);
});

app.listen(8080);

Always respond 200 OK to a callback. If your endpoint errors or is unreachable, AudD queues the callbacks and re-sends the backlog once you recover — so a transient bug won’t lose matches, but it will delay them.

Longpoll consumer (no public server needed):

const poll = await audd.streams.longpoll({ radioId: 1, timeout: 50 });

for await (const m of poll.matches) {
  // update the same nowPlaying cache the frontend reads
  console.log("now playing:", m.song.artist, "—", m.song.title);
}

Longpoll returns nothing unless the account has a callback URL set (Step 1); the SDK preflights this and raises AudDInvalidRequestError if it’s missing.

Step 4: Render it

For an OBS overlay, the simplest frontend is a small HTML page that polls /now-playing/:radioId every few seconds and re-renders when the song changes; the streamer adds it as an OBS Browser source. The now-playing widget recipe has a complete copy-paste widget.html you can use unchanged — point its BACKEND constant at the server from Step 3.

For a viewer-facing Twitch Extension, the frontend is the same idea but lives inside Twitch’s Extension iframe — see Approach B.

Approach B: fork AudD’s official Twitch Extension

AudD maintains an open-source Twitch Extension that already implements the consumer and the frontend, including a longpoll client written in JS. It’s the canonical full implementation — fork it instead of writing the frontend from scratch.

How the Extension fits together

A Twitch Extension is a small web app that Twitch hosts inside the player as a panel, a video overlay, or a component. It can’t open arbitrary inbound connections, so it works exactly like the frontend in Approach A: it reads the current song from a backend over HTTP. AudD’s Extension reads it directly from the streams longpoll endpoint.

The piece that makes a client-side longpoll safe is the longpoll category. Every stream has a longpoll_category (returned by getStreams), and the longpoll endpoint subscribes by category rather than by API token:

https://api.audd.io/longpoll/?category=<longpoll_category>&timeout=50

The category is derived from your token and the radioId but does not reveal the token, so you can hand it to the Extension’s frontend and let the browser longpoll directly — without ever shipping your API token to the client.

Never put your API token in client-side code. Share the longpoll_category with the frontend instead. The Extension reads matches by category; your token stays on your account and on any server-side calls (setCallbackUrl, addStream).

Reusing just the frontend

If you don’t want to deploy a full Twitch Extension, AudD also hosts the Extension’s frontend as a standalone widget at widget.audd.tech. It renders the last recognized song (and optionally a history strip) from a longpoll category, with no token and no backend of your own:

https://widget.audd.tech/?ch=-<longpoll_category>&background&history&shadow

Pass the longpoll_category with a - prefix as the ch parameter. This is the no-code path: subscribe the channel server-side with addStream, read its longpoll_category from getStreams, and drop the widget URL into an OBS Browser source.

What you get back

A callback (or longpoll) body for a recognized song looks like this:

{
  "status": "success",
  "result": {
    "radio_id": 1,
    "timestamp": "2020-04-13 10:31:43",
    "play_length": 111,
    "results": [
      {
        "artist": "Alan Walker, A$AP Rocky",
        "title": "Live Fast (PUBGM)",
        "album": "Live Fast (PUBGM)",
        "release_date": "2019-07-25",
        "label": "MER Recordings",
        "score": 100,
        "song_link": "https://lis.tn/LiveFastPUBGM"
      }
    ]
  }
}
FieldMeaning for the display
radio_idWhich channel this match is for — your handle from addStream.
timestampWhen AudD recognized the song.
play_lengthTotal seconds the song played. Absent with callbacks=before — the song hasn’t finished yet.
results[0].artist, .title, .albumWhat’s playing — the core of the display.
results[0].song_linkA universal lis.tn link; append ?thumb for cover art.
results[0].scoreMatch confidence.

A notification body arrives instead of a result when something happens to the channel itself: code 0 means all is well, 650 means AudD can’t connect to the stream (the channel may be offline), 651 means it’s receiving only white noise. Surface 650/651 so you know the channel went down. Any field AudD returns that the SDK doesn’t expose as a typed property is available on the result’s extras map.

Handling errors

For a Twitch now-playing display the failure modes are narrow:

  • Authentication errors (AudDAuthenticationError) — bad or missing token, or the streams add-on isn’t enabled. Fail at startup.
  • Invalid-request errors (AudDInvalidRequestError) — most commonly the longpoll preflight firing because no callback URL is set. Set one (a real URL or https://audd.tech/empty/) and retry.
  • Stream notifications (650 / 651) — not exceptions; delivered as notification bodies. A 650 usually means the channel is offline; there’s nothing to fix until it goes live again.
  • Connection errors (AudDConnectionError) — transient. The longpoll loop reconnects on its own; for callbacks, AudD queues and re-sends.
import { AudDInvalidRequestError } from "@audd/sdk";

try {
  const poll = await audd.streams.longpoll({ radioId: 1 });
  // ...
} catch (err) {
  if (err instanceof AudDInvalidRequestError) {
    // almost always: no callback URL on the account
    await audd.streams.setCallbackUrl("https://audd.tech/empty/");
  } else {
    throw err;
  }
}

Going further

  • Multiple channels. Each channel has its own radioId and longpoll_category; cache per radioId and let the frontend pick which to show, or run one widget per channel.
  • History strip. Keep the last N matches per channel and render a “recently played” list — the official Extension and the hosted widget both support this.
  • YouTube too. The same flow recognizes YouTube live streams; swap the twitch:<channel> shortcut for youtube:<video_id> or youtube-ch:<channel_id> in addStream.

Related

Reading this as an AI agent? The raw Markdown is at integrations/twitch-extension.md, and the full index is /resources/llms.txt.