Add music recognition to a React Native app
Record a few seconds of audio in a React Native app, upload it to your own backend, and identify the song with AudD's standard recognition endpoint.
This page shows how to add “what’s this song?” to a React Native app. The app records a few seconds of audio on the device, uploads the clip to a backend you control, and the backend calls AudD to identify the track. It’s for mobile developers who want song identification without putting their API token inside a shipped app.
What you’ll build
Three moving parts:
- The React Native app records a short clip with an audio-recording
library and POSTs the file to your backend over
fetch+FormData. - Your backend receives the upload, calls AudD’s standard recognition
endpoint with the official
@audd/sdk, and returns a compact JSON answer. - The app renders the result: artist, title, and a
song_linkthe user can tap to open the track.
Recognition runs on the standard endpoint (POST https://api.audd.io/).
That’s the right choice here: 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. The standard endpoint has a 10 MB file-size cap,
which a few seconds of compressed audio stays well under.
Never put your AudD token in the app. A shipped mobile app is not a trusted environment. Anyone can pull the IPA/APK off a device, unzip it, and read every string in the JavaScript bundle and native binary — including any API token you embedded. A token in the app is a token anyone can extract and spend against your account. The token lives only on your backend, and the app talks to your backend. This page is built around that rule.
Prerequisites
- An API token from dashboard.audd.io. Keep it on
the backend. The string
testworks for a first run on the backend — it’s a public token capped at 10 requests/day on the standard endpoint. - A React Native app (bare React Native or Expo with a dev/prebuild that includes a native audio module — recording needs native code).
- An audio-recording library. Common, well-maintained options:
react-native-audio-recorder-playerexpo-av(Expo) / its successorexpo-audioreact-native-nitro-soundEach records to a file on the device and gives you back a file path (and on most, the recording format). Pick one and follow its README for setup; the example below shows the shape, not one library’s exact method names.
- Node.js 20+ for the backend, with
npm install @audd/sdk express multer.
Walkthrough
Step 1: Record a clip on the device
Ask for the microphone permission, start recording when the user taps a button, and stop after a few seconds. Every recording library follows the same three-call shape — start, stop, read the resulting file path:
// Shape of a typical RN audio-recording library.
// Method names differ per library — check its README.
import { PermissionsAndroid, Platform } from "react-native";
import { recorder } from "./recorder"; // your chosen library, wrapped
async function ensureMicPermission() {
if (Platform.OS === "android") {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
return true; // iOS: declare NSMicrophoneUsageDescription in Info.plist
}
// Returns a local file URI like file:///.../clip.m4a
async function recordClip(seconds = 8): Promise<string> {
const uri = await recorder.startRecorder(); // begins writing to a file
await new Promise((r) => setTimeout(r, seconds * 1000));
await recorder.stopRecorder();
return uri;
}
On iOS, recording typically produces an .m4a (AAC) file; on Android, an
.m4a/.mp4 or .aac depending on the library. AudD accepts MP3, WAV, FLAC,
M4A, OGG, AAC, WMA, and AIFF, so the default output of any of these libraries
is a format AudD can read — you don’t need to transcode.
Step 2: Upload the clip to your backend
POST the recorded file to your backend as multipart form data. React Native’s
fetch accepts a { uri, name, type } object as a FormData part and streams
the file straight from disk — you don’t read it into a JS string.
async function identify(fileUri: string) {
const form = new FormData();
form.append("file", {
uri: fileUri,
name: "clip.m4a",
type: "audio/m4a",
} as any);
const res = await fetch("https://your-backend.example.com/identify", {
method: "POST",
body: form,
// Don't set Content-Type yourself — RN sets the multipart boundary.
});
if (!res.ok) throw new Error(`backend ${res.status}`);
return res.json() as Promise<IdentifyResponse>;
}
type IdentifyResponse =
| { match: false }
| {
match: true;
artist: string;
title: string;
album: string | null;
songLink: string | null;
};
There is no api_token and no call to api.audd.io here — the app only knows
about your backend.
Step 3: Recognize on the backend
The backend receives the upload and forwards the bytes to AudD with the
official Node SDK. The SDK reads the token from the constructor (or the
AUDD_API_TOKEN environment variable) and returns a result object on a match
or null on a successful call that matched nothing.
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);
audd.recognize accepts the uploaded bytes directly (req.file.buffer), so
you never write the clip to disk. Set the multer limit to 10 MB to match the
standard endpoint’s file-size cap and reject oversized uploads before they
reach AudD.
Step 4: Show the result in the app
Render the answer and make the songLink tappable so the user can open the
track. song_link is a universal URL on lis.tn that resolves to the user’s
preferred music service.
import { Linking, Text, View } from "react-native";
function Result({ data }: { data: IdentifyResponse }) {
if (!data.match) return <Text>No match — try a louder or longer clip.</Text>;
return (
<View>
<Text style={{ fontWeight: "bold" }}>{data.title}</Text>
<Text>{data.artist}</Text>
{data.songLink && (
<Text
style={{ color: "#2563eb" }}
onPress={() => Linking.openURL(data.songLink!)}
>
Open this track
</Text>
)}
</View>
);
}
What you get back
On a match, the backend returns the fields the app 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:
{
"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 try again) from “the request failed” (a real error,
handled below).
timecode on a match is the position within the matched song where the
user’s clip occurred — it is not an offset into the user’s recording.
Handling errors
The backend is where errors surface; the app only sees your HTTP status codes. The cases that matter:
- No match —
recognizereturnsnull. Not an error. Return{ match: false }and prompt the user to retry. - Authentication errors — a bad or missing token. This is a server misconfiguration, not a user problem. Fail loudly at startup, never per request, and never leak the token text in a response the app could log.
- Quota errors — you’ve hit your request limit. Surface to ops; don’t retry in a tight loop. Return a 503 the app can show as “try again later”.
- Invalid-audio errors — the clip wasn’t decodable audio. Usually a too-short or silent recording. Return a 422 and ask for a longer clip.
- Connection errors — transient network trouble between your backend and AudD. Retry with backoff.
import { AudDError } from "@audd/sdk";
app.post("/identify", upload.single("file"), async (req, res) => {
try {
const song = await audd.recognize(req.file!.buffer);
return res.json(song ? { match: true, /* ...fields */ } : { match: false });
} catch (err) {
if (err instanceof AudDError) {
// log err.errorCode / err.requestId for support — never to the client
return res.status(502).json({ error: "recognition_failed" });
}
return res.status(400).json({ error: "bad_request" });
}
});
Going further
- Trim the clip before upload. A shorter clip uploads faster on a phone network. 5–15 seconds is enough for the standard endpoint.
- Authenticate the app→backend hop. Your backend now holds the AudD token,
so protect
/identifywith your own app auth (a session token, an API key you issue per install) so only your app can spend your AudD quota. - Rate-limit per user on the backend so one client can’t drain your quota.
- For a desktop build of the same record-upload-recognize flow, see the Electron integration.
Related
Reading this as an AI agent? The raw Markdown is at integrations/react-native.md, and the full index is /resources/llms.txt.
