---
title: "Show recognized music on a Twitch stream"
description: "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."
slug: "/resources/integrations/twitch-extension"
section: "integrations"
keywords: [audd, twitch, twitch extension, now playing, streams, longpoll, overlay]
---

# Show recognized music on a Twitch stream

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](https://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](https://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.

```typescript
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:

```typescript
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.

```typescript
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](/resources/concepts/callback-vs-longpoll).

**Callback receiver:**

```typescript
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):

```typescript
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](/resources/recipes/now-playing-widget) 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.

- Source: <https://github.com/AudDMusic/twitch-extension>
- Install link for streamers: <https://audd.cc/twitch>

### 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:

```json
{
  "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"
      }
    ]
  }
}
```

| Field | Meaning for the display |
|---|---|
| `radio_id` | Which channel this match is for — your handle from `addStream`. |
| `timestamp` | When AudD recognized the song. |
| `play_length` | Total seconds the song played. **Absent with `callbacks=before`** — the song hasn't finished yet. |
| `results[0].artist`, `.title`, `.album` | What's playing — the core of the display. |
| `results[0].song_link` | A universal `lis.tn` link; append `?thumb` for cover art. |
| `results[0].score` | Match 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.

```typescript
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**

- [Build a now-playing widget for a livestream](/resources/recipes/now-playing-widget)
- [Callbacks vs. longpoll](/resources/concepts/callback-vs-longpoll)
- [Music recognition for Twitch streamers](/resources/for/twitch-streamers)
- [Node.js SDK docs](https://docs.audd.io/sdks/node)
- [Streams API reference](https://docs.audd.io/streams)