Integration

Run AudD music recognition in a Cloudflare Worker

Build a Cloudflare Worker that takes a URL or an uploaded file, calls the AudD API, and returns the recognized track as JSON — secret handling, fetch-based recognition, and CPU-time limits covered.

view .md auddcloudflare workersmusic recognitionedge runtime

This guide runs AudD music recognition inside a Cloudflare Worker: a request comes in carrying either a URL to identify or an uploaded audio file, the Worker calls AudD, and it returns the recognized track as JSON. It’s for anyone putting recognition at the edge — a public “what’s this song” endpoint, a thin proxy in front of your app, or a moderation hook on an upload path.

The Workers runtime is not Node. There is no filesystem and no fs module, so you cannot hand AudD a file path. You work with two inputs the runtime does give you: a URL (AudD fetches it server-side) or raw bytes you already hold in memory (from request.formData() or request.arrayBuffer()). Both map cleanly onto AudD’s HTTP API.

What you’ll build

A single Worker with one fetch handler. It accepts:

  • GET /?url=https://… — identify audio at a public URL, or
  • POST / with multipart/form-data — identify an uploaded file.

The Worker reads its AudD token from a Worker secret, calls the standard recognition endpoint (POST https://api.audd.io/), and returns the match as JSON — or { "match": null } when nothing was recognized.

The @audd/sdk package is fetch-based, so it runs on the Workers runtime directly. We’ll use it for the main path and show the plain-fetch fallback so you have it if a runtime quirk gets in the way.

Prerequisites

  • An API token from dashboard.audd.io. The string test works for the first run (standard endpoint, 10 requests/day).
  • Node.js 20+ locally and the Wrangler CLI: npm install -g wrangler.
  • A Cloudflare account (wrangler login).

Walkthrough

Step 1: Scaffold the Worker

npm create cloudflare@latest audd-worker -- --type=hello-world
cd audd-worker
npm install @audd/sdk

Your wrangler.toml needs the essentials — a name, an entry point, a recent compatibility date, and the nodejs_compat flag (the SDK and FormData behave best with it on):

name = "audd-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

Don’t put your token in wrangler.toml. It goes in as a secret in the next step.

Step 2: Store the API token as a Worker secret

The token is a credential, so store it as an encrypted Worker secret rather than a plain [vars] entry:

wrangler secret put AUDD_API_TOKEN
# paste your token when prompted

It arrives in the handler as env.AUDD_API_TOKEN. For local development, create a .dev.vars file (git-ignored) so wrangler dev can read it:

AUDD_API_TOKEN=test

Step 3: Recognize a URL

The simplest path: a caller passes ?url=, and AudD fetches and fingerprints that URL server-side. Nothing is downloaded into the Worker.

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

export interface Env {
  AUDD_API_TOKEN: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const audd = new AudD(env.AUDD_API_TOKEN);
    const url = new URL(request.url).searchParams.get("url");

    if (!url) {
      return Response.json({ error: "pass ?url= or POST a file" }, { status: 400 });
    }

    const song = await audd.recognize(url, {
      returnMetadata: ["apple_music", "spotify"],
    });

    return Response.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 and hit the example track:

wrangler dev
# in another shell:
curl "http://localhost:8787/?url=https://audd.tech/example.mp3"

You’ll get back a JSON object with the recognized artist, title, album, and a songLink (a universal lis.tn URL). A successful call that didn’t match returns { "match": null }; that’s distinct from an error.

Step 4: Recognize an uploaded file

When the caller uploads bytes instead of a URL, read them with request.formData() and pass the File/Blob straight to recognize. The SDK accepts a Blob, so there’s no filesystem involved — the bytes go up as a multipart field.

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

export interface Env {
  AUDD_API_TOKEN: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const audd = new AudD(env.AUDD_API_TOKEN);

    if (request.method === "POST") {
      const form = await request.formData();
      const file = form.get("file");

      if (!(file instanceof File)) {
        return Response.json({ error: "expected a 'file' field" }, { status: 400 });
      }

      // file is a Blob; the SDK uploads its bytes as a multipart field.
      const song = await audd.recognize(file, {
        returnMetadata: ["apple_music"],
      });

      return Response.json({ match: song ? songToJson(song) : null });
    }

    // ... GET ?url= path from Step 3 ...
    return Response.json({ error: "POST a file or GET with ?url=" }, { status: 405 });
  },
};

function songToJson(song: NonNullable<Awaited<ReturnType<AudD["recognize"]>>>) {
  return {
    artist: song.artist,
    title: song.title,
    album: song.album,
    label: song.label,
    songLink: song.songLink,
    timecode: song.timecode,
  };
}

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:8787/

Keep uploads to the standard endpoint’s 10 MB cap. Workers also impose their own request-body size limits depending on your plan, so reject oversized bodies before you read them.

Step 5: The plain-fetch fallback

The SDK is fetch-based and runs on Workers, but if a runtime quirk ever gets between you and it — a FormData edge case, a bundler issue — 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.

async function recognizeRaw(env: Env, bytes: Blob): Promise<unknown> {
  const form = new FormData();
  form.set("api_token", env.AUDD_API_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 response envelope is { status, result }: status: "error" carries error_code and error_message; on success, result is the match or null.

CPU time vs. wall-clock time

Cloudflare meters Worker CPU time, not wall-clock time. Recognition is a network call — your Worker spends almost all of its time awaiting AudD’s response, which is wall-clock, not CPU. So a standard recognition (response under 2 seconds) sits comfortably inside Workers’ CPU budget even on the free plan, because the awaiting time doesn’t count against CPU.

The thing to watch is the enterprise endpoint (audd.recognizeEnterprise(...)). Enterprise calls process long audio server-side and can run for minutes — well past a Worker’s total request-duration limit. Don’t run interactive enterprise recognition inside a Worker fetch handler.

Keep interactive Worker requests on the standard endpoint. Standard recognition returns in under 2 seconds — a good fit for a request/response Worker. Enterprise calls chunk long audio server-side and can run for minutes; route those to a queue-backed consumer (Cloudflare Queues, a Durable Object, or your own backend) instead of blocking a fetch handler.

If you do need enterprise recognition at the edge, accept the request, enqueue the job (with the source URL), return 202 Accepted, and let a separate Queue consumer call AudD and store the result. And always set limit on enterprise calls during development — the endpoint bills per 12 seconds of audio processed, and an unbounded call on a long file can ingest hours of audio.

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 that redirects to the listener’s preferred service. The provider blocks (apple_music, spotify, …) are populated only when you request them via returnMetadata. For fields outside the typed surface, read song.extras.

Handling errors

The SDK raises typed errors. In a Worker, map them to HTTP responses:

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

try {
  const song = await audd.recognize(input);
  return Response.json({ match: song ?? null });
} catch (err) {
  if (err instanceof AudDAuthenticationError) {
    // bad/missing token — your config problem, not the caller's
    return Response.json({ error: "recognition_unavailable" }, { status: 500 });
  }
  if (err instanceof AudDQuotaError) {
    return Response.json({ error: "quota_exceeded" }, { status: 429 });
  }
  if (err instanceof AudDInvalidAudioError) {
    // the upload wasn't decodable audio — a caller error
    return Response.json({ error: "unreadable_audio" }, { status: 422 });
  }
  if (err instanceof AudDAPIError) {
    // quote err.requestId in support tickets
    return Response.json({ error: "recognition_failed" }, { status: 502 });
  }
  throw err;
}

Authentication failures are a configuration problem — surface them as a 500 and check your secret, not the caller’s input. A null result is not an error; it just means nothing matched.

Deploy

wrangler deploy

Wrangler prints the deployed URL. Confirm it end to end:

curl "https://audd-worker.<your-subdomain>.workers.dev/?url=https://audd.tech/example.mp3"

You should get the same JSON match you saw under wrangler dev. Your secret is already live — wrangler secret put set it on the deployed Worker, not just locally.

Gotchas

  • No filesystem. Don’t pass a file path to recognize on Workers — there is no fs. Pass a URL string or in-memory bytes (Blob / Uint8Array).
  • nodejs_compat matters. Without the flag, FormData and a few SDK internals can behave differently. Keep it on and use a recent compatibility_date.
  • Body size limits are double. AudD caps standard uploads at 10 MB; Workers cap request bodies by plan. Reject oversized bodies before reading them into memory.
  • Enterprise doesn’t belong in a fetch handler. Its multi-minute calls outlast a Worker request. Offload to Queues or a backend, and always 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/cloudflare-workers.md, and the full index is /resources/llms.txt.