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.
npm install @audd/sdkGet 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.
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);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 -> URLIf 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 nullValid 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.
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 itFor 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.
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.
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.
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.
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();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.
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});}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.
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??"?");}The custom-catalog endpoint is not how you submit audio for recognition. For recognition, use
recognize()orrecognizeEnterprise(). 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.
Both AudD and LongpollConsumer implement Symbol.asyncDispose for
explicit resource management:
{await using audd=newAudD("...");awaitaudd.recognize("...");}// close() called automatically hereOlder runtimes can call close() manually.
MIT — see LICENSE.
- Documentation: https://docs.audd.io
- Tokens: https://dashboard.audd.io
- Email: api@audd.io