Integration

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.

view .md auddvercelnext.jsmusic recognition

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/sdk package 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, or
  • POST /api/identify with multipart/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 test works 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 set limit on 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 recognize on the Edge runtime. Pass a URL string or in-memory bytes (Blob / Uint8Array). On the Node runtime a Buffer works 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 recognizeEnterprise inline in a route handler — offload to a queue/background job, and set limit while developing.
  • test token 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.