---
title: "Build a Shazam-style music identification app"
description: "Capture a few seconds of audio from the microphone and identify the song with AudD's standard recognition endpoint, including streaming links."
slug: "/resources/recipes/shazam-clone"
section: "recipes"
keywords: [audd, music identification, shazam clone, microphone recognition, web audio]
---

# Build a Shazam-style music identification app

This recipe builds the core of a Shazam-style app: the user taps a button,
the app records a few seconds of audio from the microphone, and you tell them
what song is playing — with links to open it in Apple Music or Spotify. It's
for anyone adding "what's this song?" to a web or mobile product.

## What you'll build

Two pieces that fit together:

1. **A browser front end** that records a short clip from the microphone with
   `MediaRecorder` and POSTs the audio blob to your backend.
2. **A small Node backend** that takes the uploaded bytes and calls AudD's
   standard recognition endpoint with the official `@audd/sdk`.

The front end never sees your API token — only the backend holds it. The
backend returns a compact JSON answer (artist, title, streaming links) that
the front end renders.

Recognition runs on the **standard endpoint** (`POST https://api.audd.io/`).
That's the right choice for this task: it's built for a short audio clip,
responds in under 2 seconds, and matches against AudD's database of 160
million songs. A clip of 5–15 seconds is plenty.

## Prerequisites

- An API token from [dashboard.audd.io](https://dashboard.audd.io). The
  string `test` works for the first run — it's a public token capped at 10
  requests/day on the standard endpoint.
- Node.js 20+ for the backend, with the official SDK: `npm install @audd/sdk express`
- A browser with microphone access. `MediaRecorder` and `getUserMedia`
  require a secure context — `https://`, or `http://localhost` during
  development.

## Walkthrough

### Step 1: Confirm the backend recognizes a clip

Before wiring up a microphone, prove the recognition path works against a
known file. The SDK's `recognize` takes a URL, a filesystem path, raw bytes,
or a `Blob`, and returns the top match — or `null` on a successful call that
matched nothing.

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

// get a real token at dashboard.audd.io; "test" is capped at 10 req/day
const audd = new AudD("test");

const song = await audd.recognize("https://audd.tech/example.mp3");

if (song) {
  console.log(`${song.artist} — ${song.title}`);
} else {
  console.log("no match");
}
```

Run this and you'll see one artist/title line printed for the example file.
Once that works, you know your token and SDK install are good; everything
after this is plumbing.

### Step 2: Ask for streaming links

A Shazam-style app wants more than a title — it wants a tappable "open in
Apple Music" link. Pass `returnMetadata` to populate provider blocks. Valid
values are `apple_music`, `spotify`, `deezer`, and `musicbrainz`.
Each one you request adds a little latency, so ask only for the providers you
render.

```ts
const song = await audd.recognize("https://audd.tech/example.mp3", {
  returnMetadata: ["apple_music", "spotify"],
});

if (song) {
  console.log(song.artist, "—", song.title);
  console.log("Apple Music:", song.streamingUrl("apple_music"));
  console.log("Spotify:", song.streamingUrl("spotify"));
  console.log("Universal link:", song.songLink);
}
```

`song.songLink` is always present on a match: it's 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 is missing — see
[What you get back](#what-you-get-back).

### Step 3: Write the backend endpoint

Now expose the recognition as an HTTP endpoint your front end can POST to.
The SDK accepts a `Buffer` directly, so you can forward the uploaded bytes
without writing them to disk.

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

const app = express();
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 key line is `{ matched: false }`: AudD returns `result: null` when the
clip didn't match anything, 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.

### Step 4: Record from the microphone in the browser

The front end records a short clip with `MediaRecorder`, then POSTs the blob
to `/identify`. Keep the clip short — a handful of seconds is enough for the
standard endpoint, and it keeps you well under the 10 MB cap.

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

When you click the button, the browser asks for microphone permission,
records about seven seconds, uploads the blob, and prints the artist, title,
and streaming links. `MediaRecorder` typically produces WebM/Opus on
Chrome and Firefox; AudD decodes it server-side, so you don't need to
transcode.

> **Keep clips short and the mic close to the source.** The standard
> endpoint is tuned for a short audio clip. Seven to ten seconds of
> reasonably clean audio matches reliably; a 30-second recording mostly adds
> upload size and latency without improving the result.

## What you get back

On a match, the SDK gives you the top result. With `returnMetadata:
["apple_music", "spotify"]`, the underlying response looks like this:

```json
{
  "status": "success",
  "result": {
    "artist": "Imagine Dragons",
    "title": "Warriors",
    "album": "Smoke + Mirrors (Deluxe)",
    "release_date": "2015-02-17",
    "label": "KIDinaKORNER/Interscope Records",
    "timecode": "00:31",
    "song_link": "https://lis.tn/Warriors",
    "apple_music": { "url": "https://music.apple.com/…", "…": "…" },
    "spotify": { "external_urls": { "spotify": "https://open.spotify.com/…" }, "…": "…" }
  }
}
```

| Field | Meaning |
|---|---|
| `artist`, `title`, `album` | The recognized track. |
| `release_date`, `label` | When and by whom the recording was released. |
| `timecode` | Position *within the matched song* where the user's clip occurred — not an offset into the user's clip. A clip recorded 31 seconds into "Warriors" reports `00:31`. |
| `song_link` | Universal `lis.tn` URL. Always present on a match. Redirects the user to the song on a service they have. Your reliable fallback when a specific provider link is absent. |
| `apple_music`, `spotify`, … | Per-provider blocks, present only for the providers you named in `returnMetadata`. Shapes mirror each provider's own track object. |

In the SDK, prefer the typed helpers over reaching into provider blocks by
hand: `song.streamingUrl("apple_music")` returns the direct Apple Music URL
when you requested that block, and falls back to a `lis.tn` redirect derived
from `song_link` otherwise. `song.streamingUrls()` returns every resolvable
provider link, and `song.previewUrl()` returns a 30-second preview when one
is available.

For fields outside the typed surface (anything AudD returns that the SDK
doesn't model as a property), read `song.extras["the_key"]`, or
`song.rawResponse` for the full payload.

## Handling errors

For this recipe, separate three outcomes that are easy to conflate:

- **No match** — `recognize` resolves to `null`. Not an error. Render "try
  again," optionally with a hint to hold the phone closer to the speaker.
- **Errors the server returns** — bad token, exhausted quota, or audio it
  couldn't decode. The SDK raises typed exceptions:

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

try {
  const song = await audd.recognize(req.body, {
    returnMetadata: ["apple_music", "spotify"],
  });
  // …
} catch (err) {
  if (err instanceof AudDAuthenticationError) {
    // bad/missing token — a config problem, not a per-request one
    return res.status(500).json({ error: "server_misconfigured" });
  } else if (err instanceof AudDQuotaError) {
    // out of requests — top up at dashboard.audd.io
    return res.status(429).json({ error: "quota_exhausted" });
  } else if (err instanceof AudDInvalidAudioError) {
    // the uploaded blob wasn't decodable audio
    return res.status(422).json({ error: "unreadable_audio" });
  } else if (err instanceof AudDAPIError) {
    console.error(`AudD #${err.errorCode}: ${err.serverMessage} (request_id=${err.requestId})`);
    return res.status(502).json({ error: "recognition_failed" });
  } else if (err instanceof AudDConnectionError) {
    // transient network issue — the SDK already retried
    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, you've hit that cap; switch to a real token from the
  dashboard.

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 the request.

## Going further

- **Mobile.** The same standard endpoint backs the native SDKs. On iOS,
  record with `AVAudioRecorder` and call the Swift SDK
  ([github.com/AudDMusic/audd-swift](https://github.com/AudDMusic/audd-swift));
  on Android, capture with `MediaRecorder` and call the Kotlin SDK
  (`io.audd:audd-kotlin` on Maven Central). The recognition call and the
  `result: null` no-match contract are identical — only the recording API
  differs per platform.
- **Match against your own tracks.** If you want the app to recognize a
  private catalog (a label's unreleased promos, an artist's own back
  catalog), upload those songs to a custom catalog first; later `recognize`
  calls on the same account match against them too. Custom-catalog access is
  gated — email [api@audd.io](mailto:api@audd.io) to enable it.
- **Longer or mixed audio.** The standard endpoint returns one match. To
  identify *every* song across a longer recording (a DJ set, a podcast), use
  the enterprise endpoint instead — see
  [Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams).

---

**Related**

- [Identify music in Instagram Reels and TikTok videos](/resources/recipes/instagram-tiktok-music-id)
- [Build a copyright scanner for user-uploaded content](/resources/recipes/ugc-copyright-scanner)
- [Node.js SDK docs](https://docs.audd.io/sdks/node)
- [API reference](https://docs.audd.io)