# audd-node


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-node" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://www.npmjs.com/package/@audd/sdk" target="_blank" rel="noopener">npm</a>
</div>

Recognize music in audio clips, long broadcasts, and live streams from Node.js.

```bash
npm install @audd/sdk
```

```typescript

// get a real token at dashboard.audd.io; "test" is capped at 10 req/day
const audd = new AudD("test");
const song = await audd.recognize("https://audd.tech/example.mp3");
if (song) console.log(`${song.artist} — ${song.title}`);
```

The package is dual ESM + CJS, ships its own TypeScript types (no `@types/...`), and requires Node.js 20 or newer. Yarn / pnpm work the same: `yarn add @audd/sdk` / `pnpm add @audd/sdk`.

## Authentication

Get your API token at [dashboard.audd.io](https://dashboard.audd.io).

The token is resolved on construction:

1. Explicit constructor argument: `new AudD("token")` or `new AudD({ apiToken: "token" })`.
2. `AUDD_API_TOKEN` environment variable.
3. Otherwise the constructor throws, with a message pointing at the dashboard and the env-var name.

```typescript

const audd = new AudD("your-api-token");
// or, with AUDD_API_TOKEN exported:
const audd2 = new AudD();
```

The string `"test"` is a public token capped at 10 requests/day for standard recognition only — useful for the first hello-world.

### Token rotation

Long-running services that pull tokens from a secret manager can rotate without rebuilding the client:

```typescript
audd.setApiToken(nextToken);
```

In-flight requests finish on the previous token; subsequent calls use the new one. Empty strings are rejected. `audd.apiToken` returns the current value.

## Recognize a clip

Standard recognition — clips up to 25 seconds, 10 MB. Returns a `RecognitionResult` on a match, `null` on a successful call that didn't match.

```typescript
// URL
const song = await audd.recognize("https://audd.tech/example.mp3");

// Filesystem path (Node only)
await audd.recognize("/var/lib/clips/track.mp3");

// URL object
await audd.recognize(new URL("https://audd.tech/example.mp3"));

// Bytes
const buf = await readFile("./clip.mp3");
await audd.recognize(buf);

// Blob / File (browser or Node)
const blob = new Blob([buf], { type: "audio/mpeg" });
await audd.recognize(blob);
```

`Source` is the union of every accepted form: `string | URL | Blob | Uint8Array`. The SDK auto-detects which one you passed and re-opens it on retry.

To get streaming-service metadata back, pass `return`:

```typescript
const song = await audd.recognize(source, {
  return: ["apple_music", "spotify"],
});
```

Reading the result:

```typescript
const song = await audd.recognize(source, { return: ["apple_music", "spotify"] });
if (!song) return;

console.log(song.artist, song.title, song.album);
console.log(song.releaseDate, song.label, song.songLink);
console.log(song.timecode);                     // position in your clip

console.log(song.streamingUrl("apple_music"));  // direct link
console.log(song.streamingUrls());              // every resolvable provider
console.log(song.previewUrl());                 // 30-second preview
console.log(song.thumbnailUrl);                 // cover art, or null

// Server fields the typed schema doesn't know about:
console.log(song.extras["some_unannounced_key"]);
console.log(song.rawResponse);                  // full payload
```

`recognize` returns the top match. The full field reference — including provider sub-models, ISRC/UPC (enterprise plan only), and the helper methods (`streamingUrl`, `streamingUrls`, `previewUrl`) — lives in [the GitHub README](https://github.com/AudDMusic/audd-node#what-you-get-back).

## Process a long audio file

For files longer than 25 seconds (broadcasts, podcasts, full DJ sets), use the enterprise endpoint. The server fingerprints the upload in chunks; the SDK flattens every match into a single array.

```typescript
const matches = await audd.recognizeEnterprise("./set.mp3", {
  limit: 50,
  accurateOffsets: true,
});

for (const m of matches) {
  console.log(m.timecode, m.score, m.artist, "—", m.title);
}
```

:::warning Enterprise calls bill per 12 seconds of audio processed

Always set `limit` during development. The unbounded default can ingest many hours of audio in a single call.

:::

Other useful options: `skip` (every Nth chunk), `every` (process every Nth chunk only), `skipFirstSeconds`, `useTimecode`, `accurateOffsets`. The default per-call timeout is one hour, because enterprise uploads stream and process server-side over minutes; override via `opts.timeoutMs`. Pass `opts.signal` (`AbortSignal`) to cancel a multi-hour call.

`EnterpriseMatch` carries `score`, `timecode`, the standard metadata fields (`artist`, `title`, `album`, `releaseDate`, `label`, `songLink`, `isrc`, `upc`), `startOffset` / `endOffset`, plus `extras` and `rawResponse` for forward compatibility.

## Get streaming-service metadata

Pass `return` to populate provider blocks. Valid values: `apple_music`, `spotify`, `deezer`, `napster`, `musicbrainz`. Each adds latency.

```typescript
const song = await audd.recognize(source, {
  return: ["apple_music", "spotify", "deezer"],
});

console.log(song?.appleMusic?.url);
console.log(song?.spotify?.external_urls?.spotify);
console.log(song?.deezer?.link);
```

Provider blocks are populated only when you request them via `return`. Their shape mirrors the upstream provider's track object — see the upstream API docs (Apple Music, Spotify, Deezer, Napster, MusicBrainz) for full field references, or [the GitHub README](https://github.com/AudDMusic/audd-node#reading-additional-metadata) for the SDK's typed surface. The full provider list lives in [the AudD HTTP API docs](/).

The convenience methods `streamingUrl(provider)` and `streamingUrls()` resolve direct provider URLs when you requested the metadata block, and fall back to a `lis.tn` redirect (`{songLink}?{provider}`) when `songLink` is hosted on `lis.tn`.

## Monitor a live audio stream

A stream is a long-running upstream audio source — a radio URL, a live-TV ingest, an HLS / Icecast / SHOUTcast endpoint — that the AudD server pulls from continuously and recognizes against. You identify each stream with a `radioId` you pick.

There are two ways to consume recognition events:

- **Callbacks** — AudD POSTs each match (and each lifecycle notification) to a URL on your server. Push-style. The default and the recipe most users want.
- **Longpoll** — your code holds a GET open against `/longpoll/` and receives matches as they happen. Pull-style. For browsers, mobile clients, and anything behind NAT.

Both consumption modes live under `audd.streams`. The recipes below cover each.

## Receive callbacks from a stream

The full setup is three calls — register a callback URL, add the stream, and parse incoming bodies in your web app.

```typescript
// 1. Register where AudD should POST recognition events
await audd.streams.setCallbackUrl(
  "https://example.com/audd-callback",
  { returnMetadata: ["apple_music", "spotify"] },
);

// 2. Add the stream
const radioId = 1; // any integer you choose — your handle for this stream
await audd.streams.add({
  url: "https://radio.example.com/live.mp3",
  radioId,
});

// 3. In your web app, parse incoming POSTs (see the Express recipe below)
```

`setCallbackUrl` registers one callback URL per account — every stream on the account POSTs there. `returnMetadata` populates provider blocks in the callback payload.

`add` accepts `url`, `radioId`, and an optional `callbacks: "before"` to deliver at song start instead of song end. Use `audd.streams.setUrl(radioId, url)` to update an existing stream's upstream URL, `audd.streams.delete(radioId)` to remove it, and `audd.streams.list()` to enumerate them.

### Express recipe

Express is the canonical Node.js framework for the receiver:

```typescript

const app = express();
app.use(express.json()); // populates req.body

app.post("/audd-callback", async (req, res) => {
  try {
    const { match, notification } = await handleCallback(req);
    if (match) {
      console.log(match.song.artist, "—", match.song.title);
    } else if (notification) {
      console.log(
        "notification",
        notification.notificationCode,
        notification.notificationMessage,
      );
    }
    res.sendStatus(200);
  } catch (err) {
    console.error("invalid callback body", err);
    res.sendStatus(400);
  }
});

app.listen(8080);
```

`handleCallback(req)` reads the body off the request and parses it. It works on Web `Request`, `http.IncomingMessage`, Express / Fastify requests with a JSON middleware, and plain `{ body }` shapes — duck-typed. Returns a `ParsedCallback`:

```typescript
interface ParsedCallback {
  match: StreamCallbackMatch | null;
  notification: StreamCallbackNotification | null;
}
```

Exactly one field is non-null on success.

If you already have the body bytes (replaying from a queue, webhook proxy, etc.), use `parseCallback(body)` instead — pure function, no I/O. Throws `AudDSerializationError` if the body is malformed or carries neither `result` nor `notification`.

`StreamCallbackMatch.song` is always present (the top match). `match.alternatives` may carry variant catalog releases of the same recording — they're not lower-confidence guesses, they're alternate releases (e.g. a "feat." credit vs. the bare-artist re-release). `StreamCallbackNotification` carries `notificationCode` / `notificationMessage` for stream-lifecycle events.

Other frameworks (Fastify, Koa, Next.js API routes, etc.) — see [the GitHub README](https://github.com/AudDMusic/audd-node#handling-callback-posts).

### Longpoll preflight

:::warning Longpoll requires a configured callback URL

If no callback URL is configured for the account, longpoll might never return any events despite successful song recognition. The SDK preflights this on your first `streams.longpoll(...)` call and raises `AudDInvalidRequestError` with guidance if it's missing; pass `skipCallbackCheck: true` once you've set one.

If you only want longpoll and have no real receiver, set `https://audd.tech/empty/` — it's a placeholder URL that discards incoming POSTs:

```typescript
await audd.streams.setCallbackUrl("https://audd.tech/empty/");
```

:::

## Poll for stream events (longpoll)

When you can't expose a public callback URL — browsers, mobile clients, intranet services — open a longpoll subscription instead. Your code holds a GET open and matches arrive over HTTP.

```typescript
const radioId = 1; // any integer you choose — your handle for this stream

const poll = await audd.streams.longpoll({ radioId, timeout: 50 });

for await (const m of poll.matches) {
  console.log(m.song.artist, "—", m.song.title);
}
```

`longpoll(...)` returns a handle with three independent async-iterables filled by a background loop:

```typescript
interface LongpollPoll {
  readonly matches: AsyncIterable<StreamCallbackMatch>;
  readonly notifications: AsyncIterable<StreamCallbackNotification>;
  readonly errors: AsyncIterable<Error>;
  close(): void;
  [Symbol.asyncDispose](): Promise<void>;
}
```

`matches` carries recognition events, `notifications` carries stream-lifecycle events ("stream stopped", "can't connect", ...), and `errors` is single-shot — the first terminal failure closes all three iterables. Server keepalives are silently absorbed.

Read the three streams concurrently:

```typescript
const poll = await audd.streams.longpoll({ radioId });

await Promise.all([
  (async () => {
    for await (const m of poll.matches) {
      console.log("match", m.song.artist, m.song.title);
    }
  })(),
  (async () => {
    for await (const n of poll.notifications) {
      console.log("notif", n.notificationCode, n.notificationMessage);
    }
  })(),
  (async () => {
    for await (const e of poll.errors) {
      console.error("longpoll error", e);
    }
  })(),
]);
```

Stop the loop with `poll.close()` — idempotent. With Explicit Resource Management (Node 20.4+, TS 5.2+):

```typescript
await using poll = await audd.streams.longpoll({ radioId });
for await (const m of poll.matches) { /* ... */ }
// poll.close() runs at scope exit
```

`LongpollOptions`: `sinceTime` (resume from a server timestamp), `timeout` (server-side longpoll seconds, default `50`), `skipCallbackCheck` (bypass the preflight when you've already verified setup elsewhere — useful when running many consumers).

### Tokenless consumers

When the consumer is a browser widget, a React Native app, or an Electron renderer, you don't want to ship the API token to the client. Your server mints an opaque per-stream identifier and hands only that to the client; the client subscribes with the identifier alone.

```typescript
// On your server, with the api_token in scope:

app.get("/stream-handle/:radioId", (req, res) => {
  const handle = deriveLongpollCategory(process.env.AUDD_API_TOKEN!, +req.params.radioId);
  res.json({ handle });
});
```

In the browser / mobile / embedded code, import from the `@audd/sdk/longpoll` sub-entry — it tree-shakes the auth client and Node helpers out of the bundle, leaving a small `fetch`-only consumer:

```typescript

const { handle } = await fetch("/stream-handle/1").then((r) => r.json());

const consumer = new LongpollConsumer(handle);
const poll = consumer.iterate({ timeout: 30 });

for await (const m of poll.matches) {
  document.querySelector("#now-playing")!.textContent =
    `${m.song.artist} — ${m.song.title}`;
}
```

The handle alone authorizes the subscription. The API token never leaves your server.

## Add a song to your custom catalog

The custom-catalog endpoint adds songs to a private fingerprint database for your account. After upload, `recognize()` calls on the same account can match against your tracks.

:::warning
Custom-catalog access is gated. Contact [api@audd.io](mailto:api@audd.io) to enable it. Calls without access raise `AudDCustomCatalogAccessError` with a contact-us message.
:::

```typescript
const audioId = 1; // any integer you choose — your reference to the song
await audd.customCatalog.add({
  audioId,
  source: "/var/lib/own-tracks/track-1.mp3",
});
```

`source` accepts the same forms as `recognize`. Re-using an `audioId` re-fingerprints that slot. There is no public list / delete — track `audioId` ↔ song mappings on your side.

## Handle errors

### Idiomatic error handling

Every error raised by the SDK is a subclass of `AudDError`. The class hierarchy lets you handle whole families with one `catch`:

```typescript
  AudD,
  AudDAuthenticationError,
  AudDQuotaError,
  AudDRateLimitError,
  AudDInvalidAudioError,
  AudDCustomCatalogAccessError,
  AudDAPIError,
  AudDConnectionError,
} from "@audd/sdk";

try {
  const song = await audd.recognize(source);
  // ...
} catch (err) {
  if (err instanceof AudDAuthenticationError) {
    throw new Error(`check your token: [#${err.errorCode}] ${err.serverMessage}`);
  } else if (err instanceof AudDQuotaError) {
    console.warn(`quota exhausted: ${err.serverMessage}`);
  } else if (err instanceof AudDRateLimitError) {
    console.warn(`rate-limited: ${err.serverMessage}`);
  } else if (err instanceof AudDInvalidAudioError) {
    console.warn(`audio rejected: ${err.serverMessage}`);
  } else if (err instanceof AudDCustomCatalogAccessError) {
    console.warn("custom catalog disabled — email api@audd.io");
  } else if (err instanceof AudDAPIError) {
    console.error(`AudD #${err.errorCode}: ${err.serverMessage} (request_id=${err.requestId})`);
  } else if (err instanceof AudDConnectionError) {
    console.error(`network: ${err.message}`);
  } else {
    throw err;
  }
}
```

Every `AudDAPIError` carries `errorCode`, `serverMessage`, `httpStatus`, `requestId` (quote in support tickets), `requestedParams`, `requestMethod`, `brandedMessage`, and `rawResponse`. `AudDConnectionError` exposes the underlying error via `.cause`. `AudDSerializationError` exposes the raw body via `.rawText`.

### Retry behavior

Every call goes through a typed retry policy. The class depends on the operation:

| Class | Methods | Retries on |
|---|---|---|
| `read` | `streams.list`, `streams.getCallbackUrl`, longpoll polls | `408`, `429`, `5xx`, pre-upload connection failures, post-upload aborts |
| `recognition` | `recognize`, `recognizeEnterprise`, `advanced.*` | `5xx`, **pre-upload** connection failures only |
| `mutating` | `streams.add`, `streams.setUrl`, `streams.delete`, `streams.setCallbackUrl`, `customCatalog.add` | **pre-upload** connection failures only |

`recognition` and `mutating` deliberately don't retry once the upload has finished — repeating a metered call after the server has done the work would double-bill, and repeating a state-changing call after the side effect may have already happened could compound it.

Defaults: `maxRetries = 3`, `backoffFactorMs = 500`, `backoffMaxMs = 30_000`. Backoff is `min(backoffFactorMs * 2^attempt, backoffMaxMs)` with `[0.5x, 1.5x]` jitter. `AudDAPIError` and its subclasses are NOT retried — once the server has parsed the request enough to return a typed error, retrying is pointless.

Override at construction time:

```typescript
new AudD("token", { maxRetries: 5, backoffFactorMs: 1000 });
```

## Configuration

### Timeouts

| Endpoint | Default per-call timeout |
|---|---|
| Standard recognition (`recognize`, streams, custom catalog, advanced) | `60_000` ms |
| Enterprise recognition (`recognizeEnterprise`) | `3_600_000` ms (1 hour) |

Override per call via `opts.timeoutMs`:

```typescript
await audd.recognize(source, { timeoutMs: 10_000 });
await audd.recognizeEnterprise(source, { timeoutMs: 30 * 60_000 });
```

The timeout is an `AbortController` wired into `fetch`. When it fires the SDK raises `AudDConnectionError("Request was aborted (timeout)")`.

### Custom HTTP client

The SDK uses the global `fetch` by default. Inject your own (typed as `FetchLike = typeof globalThis.fetch`) for proxy / mTLS / instrumentation:

```typescript

const dispatcher = new ProxyAgent("http://proxy.local:3128");
const proxiedFetch: typeof fetch = (input, init) =>
  undiciFetch(input, { ...init, dispatcher }) as unknown as Promise<Response>;

const audd = new AudD("token", { fetch: proxiedFetch });
```

The custom fetch is used for both the standard endpoint and the enterprise endpoint. The longpoll loop uses it too; `LongpollConsumer` accepts the same option.

The SDK sets the `User-Agent` header on every request: `audd-node/<version> node/<node-version> (<platform>)`. AudD ops uses this to identify SDK traffic. The token is never logged or sent as a header — multipart uploads carry it as a form field.

### Cancellation

Pass an `AbortSignal` via `opts.signal` to cancel an in-flight call:

```typescript
const ac = new AbortController();
setTimeout(() => ac.abort(), 5_000);

try {
  await audd.recognize(source, { signal: ac.signal });
} catch (err) {
  if (err instanceof AudDConnectionError) {
    console.log("aborted");
  }
}
```

For `recognizeEnterprise`, this is the right hook to bail out of multi-hour calls.

:::warning
Cancelling a `recognize()` might still consume a request on the server. Once your bytes have left the SDK, the server might finish processing and bill the request anyway — `AbortSignal` only stops the local Promise from settling.
:::

### Observability

The `onEvent` constructor hook receives plain-data events at the request boundary:

```typescript
type AudDEventKind = "request" | "response" | "exception";

interface AudDEvent {
  kind: AudDEventKind;
  method: string;            // "recognize", "addStream", ...
  url: string;
  requestId: string | null;
  httpStatus: number | null;
  elapsedMs: number | null;
  errorCode: number | null;
  extras: Record<string, unknown>;
}
```

Example — feeding to an OpenTelemetry histogram:

```typescript
const histogram = metrics.createHistogram("audd_request_ms");

const audd = new AudD("token", {
  onEvent: (e) => {
    if (e.kind === "response" && e.elapsedMs !== null) {
      histogram.record(e.elapsedMs, {
        method: e.method,
        status: String(e.httpStatus ?? 0),
      });
    }
  },
});
```

Exceptions thrown from the hook are swallowed — observability hooks must never break a request. The hook never receives the API token or request body bytes.

### Token rotation

See [Token rotation](#token-rotation) under Authentication. `audd.setApiToken(newToken)` swaps the token under the hood without aborting in-flight requests.

### Calling undocumented endpoints

For AudD endpoints not yet wrapped by typed methods on this SDK, drop down to `audd.advanced.rawRequest`:

```typescript
const body = await audd.advanced.rawRequest("getCallbackUrl");
console.log(body.result);
```

The returned object is the full response envelope (`{ status, result, ... }`); the SDK doesn't unwrap it. `status: error` responses do not throw on this path — inspect `body.status` yourself if you need to. Use it as a typed escape hatch while the typed surface catches up.

---

[dashboard.audd.io](https://dashboard.audd.io) · [HTTP API reference](/) · [Other SDKs](/sdks)
