Skip to content

Repository files navigation

audd-node

Powered by AudDCIContractnpm

Official TypeScript / Node.js SDK for music recognition API: identify music from a short audio clip, a long audio file, or a live stream.

The API itself is so simple that it can easily be used even without an SDK: docs.audd.io.

Quickstart

npm install @audd/sdk

Get your API token at dashboard.audd.io.

Recognize from a URL:

import{AudD}from"@audd/sdk";constaudd=newAudD("your-api-token");constsong=awaitaudd.recognize("https://audd.tech/example.mp3");if(song){console.log(`${song.artist}${song.title}`);}

Recognize a local file (Node):

import{AudD}from"@audd/sdk";import{readFile}from"node:fs/promises";constaudd=newAudD("your-api-token");// Pass a path…constsong=awaitaudd.recognize("./clip.mp3");// …or pass bytes directly.constbytes=awaitreadFile("./clip.mp3");constsong2=awaitaudd.recognize(bytes);

A null return means the server completed the request successfully but found no match — distinct from an error, which throws.

Authentication

Pass the token literally:

constaudd=newAudD("your-api-token");

Or set AUDD_API_TOKEN in the environment and construct without arguments:

constaudd=newAudD();

For long-running services that rotate credentials, swap the token at runtime without aborting in-flight requests:

audd.setApiToken(nextToken);

What you get back

By default, recognize resolves to a typed RecognitionResult with core tags plus AudD's universal song link — no metadata-block opt-in needed:

constsong=awaitaudd.recognize("https://audd.tech/example.mp3");if(!song)return;// Core fieldsconsole.log(song.artist,song.title,song.album);console.log(song.releaseDate,song.label,song.timecode);// AudD's universal song page — links into every providerconsole.log(song.songLink);// Helpers — driven off songLink, work without any `returnMetadata` opt-inconsole.log(song.thumbnailUrl);// cover-art image, or nullconsole.log(song.streamingUrl("spotify"));// direct or lis.tn redirectconsole.log(song.streamingUrls());// map of provider -> URL

If you need provider-specific metadata blocks, opt in per call. Request only what you need — each provider you ask for adds latency:

constsong=awaitaudd.recognize("https://audd.tech/example.mp3",{returnMetadata: ["apple_music","spotify"],});console.log(song?.appleMusic?.url);// direct Apple Music linkconsole.log(song?.spotify?.uri);// spotify:track:...console.log(song?.previewUrl());// first preview across requested providers, or null

Valid returnMetadata values: apple_music, spotify, deezer, musicbrainz. Blocks are undefined when not requested.

streamingUrl(provider) prefers the direct provider URL when you requested that block via returnMetadata, then falls back to the lis.tn redirect when songLink is on lis.tn. YouTube has only the redirect path.

Reading additional metadata

Every model carries an extras map with any server-side fields outside the typed surface, plus a rawResponse of the full unparsed JSON. Use extras to read fields outside the typed surface:

console.log(song.extras);// any non-typed top-level fieldsconsole.log(song.rawResponse);// the whole result object as the server returned it

For the request side, every call accepts an extraParameters map for additional form fields the typed options don't cover:

awaitaudd.recognize(url,{returnMetadata: "apple_music",extraParameters: {some_beta_flag: "true"},});

The same extraParameters field is on RecognizeEnterpriseOptions, SetCallbackUrlOptions, and AddStreamOptions. Typed options win on collision.

Long files (enterprise)

recognizeEnterprise accepts files up to several hours and returns a flat array of matches:

constmatches=awaitaudd.recognizeEnterprise("./show.mp3",{limit: 20});for(constmofmatches){console.log(m.timecode,m.artist,m.title);}

EnterpriseMatch carries the same core tags plus score, startSeconds, endSeconds, startOffset, endOffset, isrc, upc. startSeconds and endSeconds are where this song plays in your file, in seconds (e.g. 64.2 to 71.8) — feed straight to a player or ffmpeg; computed while parsing the chunked response, and recognizeEnterprise requests accurate offsets by default so they're precise. startOffset/endOffset are the raw millisecond positions within AudD's internal 12-second scan fragment behind them. Access to isrc, upc, and score requires a Startup plan or higher — contact us for enterprise features.

The default per-call timeout is 1 hour for this endpoint (60s for standard recognition); override with timeoutMs.

Errors

Every server error is a typed exception. Use instanceof to branch:

import{AudD,AudDAPIError,AudDAuthenticationError,AudDQuotaError,AudDSubscriptionError,AudDInvalidAudioError,AudDRateLimitError,AudDConnectionError,}from"@audd/sdk";try{awaitaudd.recognize("./clip.mp3");}catch(err){if(errinstanceofAudDAuthenticationError){// 900 / 901 / 903 — token rejected}elseif(errinstanceofAudDQuotaError){// 902 — out of credits}elseif(errinstanceofAudDSubscriptionError){// 904 / 905 — endpoint not enabled on this token}elseif(errinstanceofAudDInvalidAudioError){// 300 / 400 / 500 — file unreadable / too short / unsupported}elseif(errinstanceofAudDRateLimitError){// 611 — too many requests, slow down}elseif(errinstanceofAudDConnectionError){// network failure or aborted request}elseif(errinstanceofAudDAPIError){console.error(err.errorCode,err.serverMessage,err.requestId);}else{throwerr;}}

Every AudDAPIError exposes errorCode, serverMessage, httpStatus, requestId, requestedParams, requestMethod, brandedMessage, and rawResponse. The full hierarchy lives in src/errors.ts.

Configuration

import{AudD}from"@audd/sdk";constaudd=newAudD("...token...",{maxRetries: 3,// retry budget per callbackoffFactorMs: 500,// initial backoff (ms), jittered, exponentialfetch: customFetch,// bring your own fetch (proxy, mTLS, observability)onEvent: (e)=>{// request/response/exception inspection hookconsole.log(e.method,e.httpStatus,e.elapsedMs,e.requestId);},});

Per-call cancellation via AbortSignal, including for multi-hour enterprise calls:

constcontroller=newAbortController();setTimeout(()=>controller.abort(),30_000);constmatches=awaitaudd.recognizeEnterprise("./show.mp3",{signal: controller.signal,limit: 50,});

The constructor also accepts an options-only form (new AudD({ apiToken, ... })) if you'd rather pass everything as one object — equivalent to the two-argument form above.

A single client instance handles concurrent requests fine; spin up one per process, not one per call.

Streams

Real-time recognition over a live audio stream. Once a stream is registered, AudD POSTs each match to your callback URL — or if you can't host one, drains events to a longpoll endpoint instead.

awaitaudd.streams.setCallbackUrl("https://your.app/audd-callback",{returnMetadata: ["apple_music","musicbrainz"],});awaitaudd.streams.add({url: "https://stream.example/live.m3u8",radioId: 12345,});conststreams=awaitaudd.streams.list();

Handling callback POSTs

Drop handleCallback into any HTTP handler — Express, Fastify, Hono, or the bare node:http module. It duck-types the request: a Web Request, a Node IncomingMessage, or a framework request whose body has already been parsed all work without configuration.

importexpressfrom"express";import{handleCallback}from"@audd/sdk";constapp=express();app.use(express.json());app.post("/audd-callback",async(req,res)=>{const{ match, notification }=awaithandleCallback(req);if(match){console.log(`${match.song?.artist??"?"} - ${match.song?.title??"?"} score=${match.song?.score??"?"}`);for(constaltofmatch.alternatives){// alternatives are variant catalog releases — different artist/title is possibleconsole.log(` alt: ${alt.artist} - ${alt.title}`);}}elseif(notification){console.log(`#${notification.notificationCode}${notification.notificationMessage}`);}res.json({ok: true});});

If you already have the body bytes (queue consumer, replay tool), call parseCallback(body) directly — it accepts a parsed JSON object or a JSON string and returns the same { match, notification } shape.

Per-framework wiring

The same handleCallback(req) works across Node web frameworks — register a POST route and pass the request object in.

Fastify:

importFastifyfrom"fastify";import{handleCallback}from"@audd/sdk";constapp=Fastify();app.post("/audd-callback",async(req,reply)=>{const{ match }=awaithandleCallback(req);if(match)console.log(`${match.song?.artist??"?"}${match.song?.title??"?"}`);return{ok: true};});

Koa:

importKoafrom"koa";importRouterfrom"@koa/router";importbodyParserfrom"koa-bodyparser";import{handleCallback}from"@audd/sdk";constapp=newKoa();constrouter=newRouter();app.use(bodyParser());router.post("/audd-callback",async(ctx)=>{const{ match }=awaithandleCallback(ctx.request);if(match)console.log(`${match.song?.artist??"?"}${match.song?.title??"?"}`);ctx.body={ok: true};});app.use(router.routes());

Next.js (App Router, app/api/audd-callback/route.ts):

import{NextRequest,NextResponse}from"next/server";import{handleCallback}from"@audd/sdk";exportasyncfunctionPOST(req: NextRequest){const{ match }=awaithandleCallback(req);if(match)console.log(`${match.song?.artist??"?"}${match.song?.title??"?"}`);returnNextResponse.json({ok: true});}

Receiving events without a callback URL (longpoll)

Useful when you can't expose a public HTTPS receiver. The poll handle exposes three async-iterables — matches, notifications, errors — filled by a background loop. Iterate them independently, or in parallel via Promise.all.

Before the first request the SDK runs a one-time getCallbackUrl preflight: AudD silently discards events for accounts without any callback URL set, and the preflight surfaces that as an actionable error. Pass skipCallbackCheck: true to bypass.

constradioId=1;// any integer you choose — your handle for this streamconstpoll=awaitaudd.streams.longpoll({ radioId,timeout: 30});forawait(constmofpoll.matches){console.log(m.song?.artist??"?",m.song?.title??"?");}

Consume matches and notifications concurrently:

awaitPromise.all([(async()=>{forawait(constmofpoll.matches){console.log("match:",m.song?.artist??"?",m.song?.title??"?");}})(),(async()=>{forawait(constnofpoll.notifications){console.log("notification:",n.notificationMessage);}})(),(async()=>{forawait(consterrofpoll.errors){console.error(err);poll.close();}})(),]);

poll.close() (or the await using resource-management form) tears down the background loop and completes all three iterables.

Browser / widget consumers

The audd/longpoll sub-entry exports a tokenless LongpollConsumer for front-end use. It carries no api_token — your server derives the category and ships it to the browser. Bundlers tree-shake the auth client out of the resulting bundle.

import{LongpollConsumer}from"@audd/sdk/longpoll";constconsumer=newLongpollConsumer("abc123def");constpoll=consumer.iterate({timeout: 30});forawait(constmofpoll.matches){console.log(m.song?.artist??"?",m.song?.title??"?");}

Custom catalog (advanced — not for music recognition)

The custom-catalog endpoint is not how you submit audio for recognition. For recognition, use recognize() or recognizeEnterprise(). This endpoint adds songs to your private fingerprint database. Requires special access — contact api@audd.io.

awaitaudd.customCatalog.add({audioId: 42,source: "https://example.com/my-track.mp3",});

A raw-request escape hatch is available under audd.advanced.rawRequest for endpoints not yet wrapped on this SDK.

Resource cleanup

Both AudD and LongpollConsumer implement Symbol.asyncDispose for explicit resource management:

{await using audd=newAudD("...");awaitaudd.recognize("...");}// close() called automatically here

Older runtimes can call close() manually.

License

MIT — see LICENSE.

Support

About

Official TypeScript / Node.js SDK for the AudD music recognition API

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages