Run AudD music recognition in a Vercel function
Build a Next.js route handler on Vercel that calls the AudD API — covering the Node runtime (full SDK, including bytes) and the Edge runtime (fetch-only, URL or in-memory bytes), with timeout and offload guidance.
This guide runs AudD music recognition inside a Vercel function — specifically
a Next.js App Router route handler at app/api/identify/route.ts. A request
arrives carrying either a URL to identify or an uploaded audio file, the
handler calls AudD, and it returns the recognized track as JSON. It’s for
anyone adding a recognition endpoint to a Next.js app deployed on Vercel.
Vercel gives you two runtimes, and the choice shapes how you call AudD:
- Node runtime (the default for route handlers) — a full Node environment.
You can use the
@audd/sdkpackage completely, including passing in-memory bytes from an upload. - Edge runtime — a lightweight
fetch-based runtime with no Node filesystem. Use a URL or in-memory bytes; never a file path.
The @audd/sdk package is fetch-based, so it runs on both. The difference is
that the Edge runtime has no fs, so file-path inputs are off the table
there — same constraint you hit on any edge/serverless runtime.
What you’ll build
A route handler at app/api/identify/route.ts that accepts:
GET /api/identify?url=https://…— identify audio at a public URL, orPOST /api/identifywithmultipart/form-data— identify an uploaded file.
It reads the AudD token from a Vercel environment variable, calls the standard
recognition endpoint (POST https://api.audd.io/), and returns the match as
JSON — or { "match": null } when nothing was recognized. We’ll write it once
for the Node runtime, then show what changes for Edge.
Prerequisites
- An API token from dashboard.audd.io. The string
testworks for the first run (standard endpoint, 10 requests/day). - A Next.js 14+ app (App Router) and the Vercel CLI:
npm install -g vercel. - The SDK:
npm install @audd/sdk.
Walkthrough
Step 1: Store the API token as an environment variable
Add AUDD_API_TOKEN to your Vercel project so the deployed function can read
it:
vercel env add AUDD_API_TOKEN
# paste your token; choose the environments (Production / Preview / Development)
For local development, put it in .env.local (git-ignored):
AUDD_API_TOKEN=test
The SDK reads AUDD_API_TOKEN from the environment automatically, so
new AudD() with no argument picks it up. You can also pass it explicitly.
Step 2: Recognize a URL (Node runtime)
A route handler defaults to the Node runtime. The simplest path takes a
?url= query parameter — AudD fetches and fingerprints that URL server-side,
so nothing is downloaded into the function.
// app/api/identify/route.ts
import { AudD } from "@audd/sdk";
import { NextRequest, NextResponse } from "next/server";
export const runtime = "nodejs"; // the default; stated for clarity
export async function GET(req: NextRequest) {
const audd = new AudD(process.env.AUDD_API_TOKEN!);
const url = req.nextUrl.searchParams.get("url");
if (!url) {
return NextResponse.json({ error: "pass ?url= or POST a file" }, { status: 400 });
}
const song = await audd.recognize(url, {
returnMetadata: ["apple_music", "spotify"],
});
return NextResponse.json({
match: song
? {
artist: song.artist,
title: song.title,
album: song.album,
releaseDate: song.releaseDate,
label: song.label,
songLink: song.songLink,
timecode: song.timecode,
}
: null,
});
}
Run it locally and hit the example track:
vercel dev
# in another shell:
curl "http://localhost:3000/api/identify?url=https://audd.tech/example.mp3"
You’ll get JSON with the recognized artist, title, album, and a
universal songLink on lis.tn. A successful call that didn’t match returns
{ "match": null }, distinct from an error.
Step 3: Recognize an uploaded file (Node runtime)
A POST with multipart/form-data lets a caller upload bytes. Read them with
request.formData() and pass the File/Blob straight to recognize. The
SDK accepts a Blob, so the bytes go up as a multipart field — no temp file,
no disk.
// app/api/identify/route.ts (POST handler)
import { AudD } from "@audd/sdk";
import { NextRequest, NextResponse } from "next/server";
export const runtime = "nodejs";
export async function POST(req: NextRequest) {
const audd = new AudD(process.env.AUDD_API_TOKEN!);
const form = await req.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "expected a 'file' field" }, { status: 400 });
}
// On the Node runtime you could also hand the SDK a Buffer:
// const buf = Buffer.from(await file.arrayBuffer());
// await audd.recognize(buf);
// Passing the Blob/File directly works on both runtimes.
const song = await audd.recognize(file, { returnMetadata: ["apple_music"] });
return NextResponse.json({
match: song
? {
artist: song.artist,
title: song.title,
album: song.album,
label: song.label,
songLink: song.songLink,
timecode: song.timecode,
}
: null,
});
}
Test it with the example file:
curl -fsSL https://audd.tech/example.mp3 -o /tmp/clip.mp3
curl -F "file=@/tmp/clip.mp3" http://localhost:3000/api/identify
Keep uploads under the standard endpoint’s 10 MB cap, and check Vercel’s body size limit for your plan before reading large bodies into memory.
Step 4: Switch to the Edge runtime
To run the same handler on the Edge runtime, change one line:
export const runtime = "edge";
The ?url= path and the request.formData() upload path both work unchanged —
they rely only on web-standard fetch, FormData, and Blob, which the Edge
runtime provides. The SDK is fetch-based, so it runs there too.
The one thing you cannot do on Edge is pass a file path to recognize —
there is no Node filesystem. Use a URL or the in-memory Blob/Uint8Array
from the upload. (The commented Buffer.from(...) line in Step 3 is Node-only;
on Edge, pass the File/Blob directly, which is what the example already
does.)
Step 5: The plain-fetch fallback
The SDK runs on both runtimes, but if a runtime quirk ever gets in the way —
a bundling issue, a FormData edge case — you can call the HTTP API directly.
There’s no special client; it’s a multipart POST to https://api.audd.io/
with api_token as a form field. This snippet works identically on Node and
Edge.
async function recognizeRaw(token: string, bytes: Blob): Promise<unknown> {
const form = new FormData();
form.set("api_token", token);
form.set("return", "apple_music,spotify");
form.set("file", bytes, "clip.mp3");
const res = await fetch("https://api.audd.io/", { method: "POST", body: form });
const body = (await res.json()) as { status: string; result: unknown };
if (body.status === "error") {
throw new Error(`AudD error: ${JSON.stringify(body)}`);
}
return body.result; // null on no match
}
To recognize a URL this way, set form.set("url", "https://…") instead of the
file field. The envelope is { status, result }: status: "error" carries
error_code and error_message; on success, result is the match or null.
What you get back
A match serializes to the core tags plus a universal link:
{
"match": {
"artist": "Imagine Dragons",
"title": "Warriors",
"album": "Smoke + Mirrors (Deluxe)",
"releaseDate": "2015-02-17",
"label": "KIDinaKORNER/Interscope Records",
"songLink": "https://lis.tn/Warriors",
"timecode": "00:31"
}
}
timecode is the position within the matched track where the clip occurred —
not an offset into the caller’s clip. songLink is a universal lis.tn URL.
Provider blocks (apple_music, spotify, …) appear only when you request them
via returnMetadata. Fields outside the typed surface are on song.extras.
Function timeouts and long enterprise scans
Vercel functions have a maximum execution duration (the ceiling depends on your
plan and runtime). Standard recognition returns in under 2 seconds, so an
interactive recognize() call sits comfortably inside any plan’s limit — use
the standard endpoint for anything a user is waiting on.
The enterprise endpoint (audd.recognizeEnterprise(...)) is different. It
chunks long audio server-side and can run for minutes — long enough to blow
past a function’s timeout. Don’t call it inline in a request a user is waiting
on.
Use the standard endpoint for interactive requests; offload enterprise scans. Standard recognition (under 2 s) fits inside a function timeout. Enterprise recognition chunks long audio server-side and can run for minutes — accept the request, enqueue the job, return
202 Accepted, and let a background worker call AudD and store the result. And always setlimiton enterprise calls during development; the endpoint bills per 12 seconds of audio processed.
A typical offload shape on Vercel: the route handler validates the request and
pushes a job (with the source URL) onto a queue — Vercel Queues, an external
queue, or a database row polled by a cron-triggered function — then a separate
worker runs recognizeEnterprise with a limit set and writes the tracklist
back. The interactive endpoint stays fast; the long scan runs out of band.
Handling errors
The SDK raises typed errors. Map them to HTTP responses:
import {
AudDAuthenticationError,
AudDQuotaError,
AudDInvalidAudioError,
AudDAPIError,
} from "@audd/sdk";
import { NextResponse } from "next/server";
try {
const song = await audd.recognize(input);
return NextResponse.json({ match: song ?? null });
} catch (err) {
if (err instanceof AudDAuthenticationError) {
// bad/missing token — your config problem
return NextResponse.json({ error: "recognition_unavailable" }, { status: 500 });
}
if (err instanceof AudDQuotaError) {
return NextResponse.json({ error: "quota_exceeded" }, { status: 429 });
}
if (err instanceof AudDInvalidAudioError) {
// upload wasn't decodable audio — a caller error
return NextResponse.json({ error: "unreadable_audio" }, { status: 422 });
}
if (err instanceof AudDAPIError) {
// quote err.requestId in support tickets
return NextResponse.json({ error: "recognition_failed" }, { status: 502 });
}
throw err;
}
Authentication failures are a configuration problem — return a 500 and check
your environment variable, not the caller’s input. A null result is not an
error; it means nothing matched.
Deploy
vercel --prod
Vercel prints the deployment URL. Confirm the endpoint end to end:
curl "https://<your-app>.vercel.app/api/identify?url=https://audd.tech/example.mp3"
You should get the same JSON match you saw under vercel dev. The
AUDD_API_TOKEN you added in Step 1 is already wired into the deployed
function.
Gotchas
- No filesystem on Edge. Don’t pass a file path to
recognizeon the Edge runtime. Pass a URL string or in-memory bytes (Blob/Uint8Array). On the Node runtime aBufferworks too. - Pick the runtime per route.
export const runtime = "edge"/"nodejs"is per-route-handler. A route that needs Node-only APIs stays on"nodejs"; a pure-recognition route can run on either. - Body size limits. AudD caps standard uploads at 10 MB; Vercel caps request bodies by plan. Reject oversized bodies before reading them.
- Enterprise blows past timeouts. Don’t run
recognizeEnterpriseinline in a route handler — offload to a queue/background job, and setlimitwhile developing. testtoken is standard-only. It works for these examples but is capped at 10 requests/day and does not work on the enterprise or streams endpoints.
Related
Reading this as an AI agent? The raw Markdown is at integrations/vercel-functions.md, and the full index is /resources/llms.txt.
