---
title: "Build a Chrome extension that identifies the music playing in a tab"
description: "Capture a few seconds of tab audio with a Manifest V3 Chrome extension, send it to your own backend, and identify the song with AudD."
slug: "/resources/integrations/chrome-extension"
section: "integrations"
keywords: [audd, chrome extension, manifest v3, tabcapture, music recognition]
---

# Build a Chrome extension that identifies the music playing in a tab

This page shows how to build a Manifest V3 Chrome extension that captures the
audio playing in the current tab, identifies the song with AudD, and shows the
answer in the extension popup. It's for developers who want "what's this song
in this tab?" — for a YouTube video, a web radio player, or any page that
plays audio.

AudD maintains an official open-source Chrome extension that does exactly this:
[github.com/AudDMusic/chrome-extension](https://github.com/AudDMusic/chrome-extension).
Treat it as the canonical, working example. This page explains the
architecture and the parts that matter so you can build your own or read
theirs with context.

## What you'll build

Three moving parts:

1. **The extension** captures a few seconds of the tab's audio with
   `chrome.tabCapture`, encodes it to a clip, and POSTs the bytes to a backend
   you control.
2. **Your backend** calls AudD's standard recognition endpoint with the
   official `@audd/sdk` and returns a compact JSON answer.
3. **The popup** renders the result: artist, title, and a tappable `song_link`.

Recognition runs on the **standard endpoint** (`POST https://api.audd.io/`).
It's built for a short audio clip, responds in under 2 seconds, and matches
against AudD's database of 160 million songs. A 5–15 second clip is plenty and
stays well under the standard endpoint's 10 MB file-size cap.

> **Never put your AudD token in the extension.** A Chrome extension ships its
> full source to every user — anyone can open `chrome://extensions`, click
> "Inspect", or just unzip the `.crx` and read every line of JavaScript,
> including any token you hard-coded or fetched into the extension. A token in
> the extension is a token anyone can extract and spend against your account.
> The token lives **only** on your backend; the extension sends audio bytes to
> your backend and gets back a result. This page is built around that rule.

## Prerequisites

- An API token from [dashboard.audd.io](https://dashboard.audd.io). Keep it on
  the backend. The string `test` works for a first run on the backend — it's a
  public token capped at 10 requests/day on the standard endpoint.
- Chrome (or any Chromium browser) with developer mode for loading an unpacked
  extension.
- Node.js 20+ for the backend, with `npm install @audd/sdk express`.

## Manifest V3 essentials

Manifest V3 extensions have no persistent background page — they run a
**service worker** that the browser starts on demand and stops when idle. Tab
audio capture needs two permissions: `tabCapture` to read the tab's audio
stream, and `activeTab` so the capture is scoped to the tab the user invoked
the extension on.

```json
{
  "manifest_version": 3,
  "name": "Tab Song ID",
  "version": "1.0.0",
  "permissions": ["tabCapture", "activeTab"],
  "host_permissions": ["https://your-backend.example.com/*"],
  "background": { "service_worker": "service-worker.js" },
  "action": { "default_popup": "popup.html" }
}
```

`host_permissions` lists **your backend** — the only network endpoint the
extension talks to. It does not list `api.audd.io`, because the extension never
calls AudD directly.

## Walkthrough

### Step 1: Capture the tab's audio

`chrome.tabCapture.capture` hands you a `MediaStream` of the tab's audio. There
is one Chrome quirk worth knowing: capturing a tab mutes its normal playback,
so route the captured stream back to the speakers through an `AudioContext`
while you also record it — otherwise the user hears silence during capture.

```js
function captureTab() {
  return new Promise((resolve, reject) => {
    chrome.tabCapture.capture({ audio: true, video: false }, (stream) => {
      if (!stream) return reject(new Error(chrome.runtime.lastError?.message));

      // Keep the tab audible while we capture.
      const ctx = new AudioContext();
      ctx.createMediaStreamSource(stream).connect(ctx.destination);

      resolve(stream);
    });
  });
}
```

### Step 2: Record a few seconds to a clip

Record the captured stream with `MediaRecorder`, collect the chunks, and stop
after a few seconds. `MediaRecorder` in Chrome produces a WebM/Opus container
by default — AudD accepts OGG/WebM-style Opus and the common audio formats
(MP3, WAV, FLAC, M4A, OGG, AAC, WMA, AIFF), so the default output works without
transcoding. Pick whichever supported `mimeType` Chrome reports as available
rather than hard-coding one.

```js
async function recordClip(stream, seconds = 8) {
  const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
    ? "audio/webm;codecs=opus"
    : "audio/webm";

  const recorder = new MediaRecorder(stream, { mimeType });
  const chunks = [];
  recorder.ondataavailable = (e) => e.data.size && chunks.push(e.data);

  return new Promise((resolve) => {
    recorder.onstop = () => {
      stream.getTracks().forEach((t) => t.stop()); // release the tab
      resolve(new Blob(chunks, { type: mimeType }));
    };
    recorder.start();
    setTimeout(() => recorder.stop(), seconds * 1000);
  });
}
```

Stop the stream's tracks when you're done so the tab is released and playback
returns to normal.

### Step 3: Send the clip to your backend

POST the recorded `Blob` to your backend as multipart form data. Again: the
target is **your** backend, with no AudD token and no `api.audd.io` in sight.

```js
async function identify(blob) {
  const form = new FormData();
  form.append("file", blob, "clip.webm");

  const res = await fetch("https://your-backend.example.com/identify", {
    method: "POST",
    body: form,
  });
  if (!res.ok) throw new Error(`backend ${res.status}`);
  return res.json();
}
```

Where this code runs matters under MV3. Tab capture is usually driven from the
popup or an offscreen document; the service worker coordinates but can be
stopped mid-task if it goes idle, so keep the capture-record-upload sequence in
a context that stays alive while recording (the popup, or an offscreen
document the worker creates).

### Step 4: Recognize on the backend

The backend forwards the uploaded bytes to AudD with the official Node SDK. The
token is read from `AUDD_API_TOKEN` and never leaves the server. `recognize`
returns a result object on a match or `null` on a successful call that matched
nothing.

```ts
import express from "express";
import multer from "multer";
import { AudD } from "@audd/sdk";

const upload = multer({ limits: { fileSize: 10 * 1024 * 1024 } }); // 10 MB cap
const audd = new AudD(process.env.AUDD_API_TOKEN!); // token lives here, only here
const app = express();

app.post("/identify", upload.single("file"), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: "no_file" });

  const song = await audd.recognize(req.file.buffer, {
    returnMetadata: ["apple_music", "spotify"], // optional streaming links
  });

  if (!song) return res.json({ match: false });

  res.json({
    match: true,
    artist: song.artist,
    title: song.title,
    album: song.album,
    songLink: song.songLink, // universal lis.tn URL
  });
});

app.listen(3000);
```

### Step 5: Show the result in the popup

Render the answer in `popup.html` and make the `songLink` open in a new tab.
Build the popup nodes with `textContent` and DOM methods rather than
`innerHTML` — extension popups are subject to a strict content security policy,
and setting markup from a string is exactly what it blocks.

```js
function render(data) {
  const el = document.getElementById("result");
  el.replaceChildren();

  if (!data.match) {
    el.textContent = "No match — try a longer clip or raise the volume.";
    return;
  }

  const title = document.createElement("strong");
  title.textContent = data.title;
  const artist = document.createElement("div");
  artist.textContent = data.artist;
  el.append(title, artist);

  if (data.songLink) {
    const a = document.createElement("a");
    a.href = data.songLink;
    a.target = "_blank";
    a.rel = "noopener";
    a.textContent = "Open this track";
    el.append(a);
  }
}
```

## What you get back

On a match, the backend returns the fields the popup needs. The AudD result
carries more — `album`, `release_date`, `label`, `timecode`, and per-provider
streaming blocks when you pass `returnMetadata` — and you forward whichever
your UI uses:

```json
{
  "match": true,
  "artist": "Imagine Dragons",
  "title": "Warriors",
  "album": "Smoke + Mirrors (Deluxe)",
  "songLink": "https://lis.tn/Warriors"
}
```

A successful call that matched nothing returns `{ "match": false }` — the SDK
gives you `null`, which is not an error. Distinguish "we recognized
nothing" (ask the user to retry) from "the request failed" (a real error).

`timecode` on a match is the position **within the matched song** where the
captured clip occurred — not an offset into your recording.

## Handling errors

Errors surface on the backend; the popup only sees your HTTP status codes.

- **No match** — `recognize` returns `null`. Not an error. Return
  `{ match: false }` and prompt a retry.
- **Authentication errors** — a bad or missing token. A server
  misconfiguration; fail loudly at startup, not per request, and never echo
  the token in a response.
- **Quota errors** — you've hit your request limit. Surface to ops; don't
  retry in a tight loop.
- **Invalid-audio errors** — the clip wasn't decodable (often a silent tab or
  a too-short capture). Return a 422 and ask for a longer clip.
- **Connection errors** — transient network trouble between backend and AudD.
  Retry with backoff.

On the extension side, the common failures are `chrome.runtime.lastError` from
`tabCapture` (no `activeTab` grant, or a restricted page like `chrome://`) and
an empty recording (the tab wasn't playing audio). Check both before uploading.

## Going further

- **Read the official extension.** [AudDMusic/chrome-extension](https://github.com/AudDMusic/chrome-extension)
  is the canonical implementation of this flow — capture, encode, recognize,
  display — and a good reference for the MV3 offscreen-document and
  service-worker wiring.
- **Authenticate the extension→backend hop.** Your backend holds the AudD
  token, so protect `/identify` with your own auth so only your extension can
  spend your quota.
- **Rate-limit per user** on the backend so one client can't drain your quota.

---

**Related**

- [Build a Shazam-style music identification app](/resources/recipes/shazam-clone)
- [AudD official Chrome extension (open source)](https://github.com/AudDMusic/chrome-extension)
- [Node.js SDK docs](https://docs.audd.io/sdks/node)
- [API reference](https://docs.audd.io)