---
title: "How to add real-time song recognition to a web application"
description: "Capture microphone audio in the browser, send it to a small backend that calls AudD's recognition API with the official SDK, and display the matched song in seconds — without exposing your API token."
slug: "/resources/articles/real-time-song-recognition-web-app"
section: "articles"
keywords: [audd, real-time recognition, web audio, mediarecorder, song identification, javascript]
---

# How to add real-time song recognition to a web application

Building a web app that recognizes music in real time means handling two things:
capturing audio in the browser, and connecting to a reliable recognition API.
The common traps are over-engineering the audio processing and putting your API
token in client-side code.

This guide shows a clean version: capture a short clip from the microphone with
`MediaRecorder`, POST it to a small backend, and let the backend call AudD's
standard endpoint with the official Node SDK. The browser never sees your token,
and you get the matched song back in a couple of seconds.

## Architecture: keep the token on the server

The single most important decision is that **your API token lives only on the
backend.** A browser bundle is public; anything you embed in it can be read and
reused. So the flow is:

1. The **browser** records a few seconds of audio and POSTs the blob to your own
   server.
2. Your **backend** forwards the bytes to AudD with the SDK and returns a
   compact JSON answer.
3. The **browser** renders that answer.

Recognition runs on the **standard endpoint** (`api.audd.io`): it's built for a
short audio clip, responds in under two seconds, and matches against AudD's
public database of **160 million songs**. The endpoint analyzes up to about 12
seconds of audio, and within that range more audio generally helps; a clip in
that range keeps you well under the endpoint's ~10 MB cap.

## Step 1: Capture audio in the browser

Use `MediaRecorder` to record a short clip and POST the resulting blob. This is
far simpler — and more reliable across browsers — than wiring up
`ScriptProcessorNode` and hand-encoding PCM. AudD decodes the recorder's output
(typically WebM/Opus on Chrome and Firefox) server-side, so you don't transcode
anything.

```html
<button id="listen">Identify song</button>
<pre id="out"></pre>

<script type="module">
const out = document.getElementById("out");

document.getElementById("listen").addEventListener("click", async () => {
  out.textContent = "Listening…";

  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const recorder = new MediaRecorder(stream);
  const chunks = [];

  recorder.addEventListener("dataavailable", (e) => chunks.push(e.data));

  recorder.addEventListener("stop", async () => {
    stream.getTracks().forEach((t) => t.stop()); // release the mic
    const blob = new Blob(chunks, { type: recorder.mimeType });

    const res = await fetch("/identify", {
      method: "POST",
      headers: { "Content-Type": "audio/webm" },
      body: blob,
    });
    const data = await res.json();

    if (!data.matched) {
      out.textContent = "No match — try again with the music a bit louder.";
      return;
    }
    out.textContent =
      `${data.artist} — ${data.title}\n` +
      `Apple Music: ${data.appleMusic ?? data.songLink}\n` +
      `Spotify: ${data.spotify ?? data.songLink}`;
  });

  recorder.start();
  setTimeout(() => recorder.stop(), 7000); // record ~7 seconds
});
</script>
```

`getUserMedia` and `MediaRecorder` require a secure context — `https://` in
production, or `http://localhost` during development.

## Step 2: Recognize on the backend

The backend takes the uploaded bytes and calls `recognize`. The SDK accepts a
`Buffer` directly, so you can forward the upload without writing it to disk.

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

const app = express();
// get your own token at dashboard.audd.io; "test" is capped at 10 requests/day
const audd = new AudD(process.env.AUDD_API_TOKEN ?? "test");

// Accept a raw audio body up to the standard endpoint's 10 MB cap.
app.use(express.raw({ type: "audio/*", limit: "10mb" }));

app.post("/identify", async (req, res) => {
  try {
    const song = await audd.recognize(req.body, {
      returnMetadata: ["apple_music", "spotify"],
    });

    if (!song) {
      // A successful call that matched nothing — NOT an error.
      return res.json({ matched: false });
    }

    res.json({
      matched: true,
      artist: song.artist,
      title: song.title,
      album: song.album,
      songLink: song.songLink,
      appleMusic: song.streamingUrl("apple_music"),
      spotify: song.streamingUrl("spotify"),
      artwork: song.thumbnailUrl,
    });
  } catch (err) {
    console.error("recognition failed", err);
    res.status(502).json({ error: "recognition_failed" });
  }
});

app.listen(8080, () => console.log("listening on :8080"));
```

The `{ matched: false }` branch is the one to get right: AudD returns
`result: null` when the clip matched nothing, and the SDK surfaces that as
`null` from `recognize`. That is a normal, successful outcome — a quiet room, a
song not in the database, a clip that's mostly speech. Treat it as "try again,"
never as a server error.

`song.songLink` is always present on a match — a universal `lis.tn` URL that
redirects to the song on whatever service the user has. Use it as the fallback
when a specific provider link (`streamingUrl("apple_music")`) is missing.

## Step 3: Recognize continuously

For a "keep listening" experience, record on a loop instead of once. The pattern
that keeps this cheap and correct is a single in-flight request plus a minimum
interval between calls — record a clip, send it, wait for the answer, pause
briefly, repeat. Recognizing a fresh clip every several seconds is plenty;
firing a request on every audio frame just burns quota for the same song.

```javascript
let busy = false;

async function recognizeOnce(blob) {
  if (busy) return;          // one request in flight at a time
  busy = true;
  try {
    const res = await fetch("/identify", {
      method: "POST",
      headers: { "Content-Type": "audio/webm" },
      body: blob,
    });
    const data = await res.json();
    if (data.matched) showNowPlaying(data);
  } finally {
    busy = false;
  }
}
```

Skip a recognition while one is already in flight, and leave a few seconds
between successful calls. The same song will keep matching, so de-duplicate on
`artist` + `title` before updating the UI.

## Step 4: Handle errors and edge cases

Separate three outcomes:

- **No match** — `recognize` returns `null`. Not an error. Prompt the user to
  try again, ideally with the mic closer to the source.
- **Server-returned errors** — a bad token, exhausted quota, or undecodable
  audio. The SDK raises typed exceptions you can branch on:

```javascript
import {
  AudDAuthenticationError,
  AudDQuotaError,
  AudDInvalidAudioError,
  AudDAPIError,
  AudDConnectionError,
} from "@audd/sdk";

try {
  const song = await audd.recognize(req.body);
  // …
} catch (err) {
  if (err instanceof AudDAuthenticationError) {
    return res.status(500).json({ error: "server_misconfigured" });
  } else if (err instanceof AudDQuotaError) {
    return res.status(429).json({ error: "quota_exhausted" });
  } else if (err instanceof AudDInvalidAudioError) {
    return res.status(422).json({ error: "unreadable_audio" });
  } else if (err instanceof AudDAPIError) {
    return res.status(502).json({ error: "recognition_failed" });
  } else if (err instanceof AudDConnectionError) {
    return res.status(504).json({ error: "upstream_timeout" });
  }
  throw err;
}
```

- **Quota during testing** — the `test` token is capped at 10 requests/day on
  the standard endpoint. If recognition suddenly starts returning a quota error
  in development, switch to a real token from
  [dashboard.audd.io](https://dashboard.audd.io).

The SDK retries transient connection failures before the upload completes, but
never repeats a recognition call after your bytes reached the server —
re-sending would risk double-billing.

## Production checklist

- **HTTPS** — browser audio APIs require a secure context.
- **Token on the server only** — never ship `AUDD_API_TOKEN` in client code; the
  backend pattern above is what keeps it private.
- **Browser support** — feature-detect `navigator.mediaDevices.getUserMedia` and
  `MediaRecorder`, and degrade gracefully where they're missing.
- **Permissions** — handle a denied microphone prompt with a clear message.
- **Rate** — keep one request in flight and a sensible interval between calls so
  continuous listening doesn't burn quota on the same song.

The standard endpoint returns the single best match for a clip, typically within
two seconds — fast enough that users get immediate feedback. If you instead need
to identify *every* song across a longer recording (a DJ set, a podcast), reach
for the enterprise endpoint, which chunks the file and returns every match with
timestamps. See
[Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams).

Get a token at [dashboard.audd.io](https://dashboard.audd.io) and read the
[API reference](https://docs.audd.io) for the full set of options.

---

**Related**

- [Build a Shazam-style music identification app](/resources/recipes/shazam-clone)
- [Build a now-playing widget](/resources/recipes/now-playing-widget)
- [Node.js SDK docs](https://docs.audd.io/sdks/node)
- [API reference](https://docs.audd.io)