---
title: "Run AudD music recognition in a Vercel function"
description: "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."
slug: "/resources/integrations/vercel-functions"
section: "integrations"
keywords: [audd, vercel, next.js, music recognition, edge runtime, route handler]
---

# Run AudD music recognition in a Vercel function

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](https://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:

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

```ts
// 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:

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

```ts
// 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:

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

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

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

```json
{
  "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:

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

```bash
vercel --prod
```

Vercel prints the deployment URL. Confirm the endpoint end to end:

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

- [Run AudD music recognition in a Cloudflare Worker](/resources/integrations/cloudflare-workers)
- [Build a Shazam-style song identifier](/resources/recipes/shazam-clone)
- [Node.js SDK docs](https://docs.audd.io/sdks/node)
- [API reference](https://docs.audd.io)