audd-dotnet
Recognize music in audio clips, long broadcasts, and live streams from .NET / C#.
dotnet add package AudD
using AudD;
await using var audd = new AudD("test"); // 10 reqs/day; get a real token at dashboard.audd.io
var result = await audd.RecognizeAsync("https://audd.tech/example.mp3");
if (result is not null)
{
Console.WriteLine($"{result.Artist} — {result.Title}");
}
Authentication
Get your API token at dashboard.audd.io.
The api_token is resolved on construction in this order:
- The
apiTokenconstructor argument. - The
AUDD_API_TOKENenvironment variable. - Otherwise,
ArgumentExceptionwith a pointer to dashboard.audd.io.
await using var audd = new AudD("your-api-token"); // explicit
await using var audd = AudD.FromEnvironment(); // reads AUDD_API_TOKEN
AudD.TokenEnvVar exposes the env-var name ("AUDD_API_TOKEN") for tooling.
The public "test" token is capped at 10 requests/day and works only for standard recognition — fine for hello-worlds, not for production.
For long-running services pulling tokens from a secret manager, rotate without rebuilding the client:
audd.SetApiToken("new-token");
SetApiToken is thread-safe (Interlocked.Exchange under the hood). In-flight requests finish on the token they captured at send time; subsequent calls use the new one. The current token is readable via audd.ApiToken.
Recognize a clip
RecognizeAsync takes a 5–25-second clip and returns a single RecognitionResult on a match, or null when the clip processed but matched nothing.
await using var audd = new AudD("your-api-token");
// URL — server fetches the audio
var result = await audd.RecognizeAsync("https://audd.tech/example.mp3");
// Filesystem path
result = await audd.RecognizeAsync(new FileInfo("/clip.mp3"));
// byte[]
var bytes = await File.ReadAllBytesAsync("/clip.mp3");
result = await audd.RecognizeAsync(bytes);
// Stream (must be seekable for retry)
await using var fs = File.OpenRead("/clip.mp3");
result = await audd.RecognizeAsync(fs);
if (result is not null)
{
Console.WriteLine($"{result.Artist} — {result.Title} @ {result.Timecode}");
Console.WriteLine($"song page: {result.SongLink}");
Console.WriteLine($"cover art: {result.ThumbnailUrl}");
}
A RecognitionResult is a record carrying Artist, Title, Album, ReleaseDate, Label, Timecode, SongLink, Isrc / Upc (enterprise plans), and helpers — ThumbnailUrl, StreamingUrl(provider), StreamingUrls(), PreviewUrl(). Server fields not yet typed surface via result.Extras (a Dictionary<string, JsonElement> wired with [JsonExtensionData]). Full reference: github.com/AudDMusic/audd-dotnet#what-you-get-back.
Source-form notes: a string that is neither an http(s):// URL nor an existing path raises ArgumentException. FileInfo reopens a fresh FileStream on each retry. byte[] copies on each retry. Stream records Position at first call and seeks back on retries — unseekable streams cannot be retried; buffer to byte[] first if you need that. The SDK never disposes a Stream you pass it.
A separate cancellationToken parameter on every method honors standard .NET cancellation; the optional timeout: TimeSpan.FromSeconds(...) is layered on top via CancelAfter.
Process a long audio file
For broadcasts, podcasts, and full sets longer than 25 seconds, use the enterprise endpoint. It chunks the file server-side and returns every match.
var matches = await audd.RecognizeEnterpriseAsync(
new FileInfo("/path/to/broadcast.mp3"),
limit: 10); // stop after 10 matches; ALWAYS set this in development
foreach (var m in matches)
{
Console.WriteLine($"{m.Timecode} {m.Artist} — {m.Title} (score={m.Score})");
}
Other accepted args: skip, every, skipFirstSeconds, useTimecode, accurateOffsets, timeout. The endpoint accepts the same source forms as RecognizeAsync (URL string, FileInfo, byte[], Stream).
Always set limit: during development. The unbounded default can ingest many hours of audio on a single call.
The SDK streams the upload — multi-GB files are not buffered fully in memory. The default per-request timeout for the enterprise endpoint is 1 hour; long files take that long to fingerprint server-side.
Each EnterpriseMatch is a record with Score, Timecode, Artist, Title, Album, ReleaseDate, Label, SongLink, Isrc / Upc (enterprise plans), StartOffset, EndOffset, plus ThumbnailUrl, StreamingUrl(provider), StreamingUrls(). Enterprise matches don't carry per-provider metadata blocks — StreamingUrl returns the lis.tn redirect path only.
Get streaming-service metadata
Pass @return: to populate provider sub-objects on the result. Without it, those fields are null.
var result = await audd.RecognizeAsync(
"https://audd.tech/example.mp3",
@return: new[] { "apple_music", "spotify" });
if (result is not null)
{
if (result.AppleMusic is not null)
Console.WriteLine($"Apple Music: {result.AppleMusic.Url}");
if (result.Spotify is not null)
Console.WriteLine($"Spotify URI: {result.Spotify.Uri}");
// Or resolve any provider — direct URL when the metadata block is set,
// else the lis.tn redirect when SongLink is on lis.tn.
Console.WriteLine(result.StreamingUrl(StreamingProvider.Spotify));
foreach (var (p, url) in result.StreamingUrls())
Console.WriteLine($"{p}: {url}");
Console.WriteLine(result.PreviewUrl()); // 30-second preview, provider terms apply
}
Valid providers: apple_music, spotify, deezer, napster, musicbrainz. Each provider adds latency.
The market: argument (ISO 3166 region code) controls the Apple Music storefront:
var result = await audd.RecognizeAsync(url, @return: new[] { "apple_music" }, market: "GB");
For the field shape of each provider sub-object, see the upstream API reference: docs.audd.io and the GitHub README.
Monitor a live audio stream
A stream in AudD is a long-running audio source — typically a radio URL, HLS, Icecast, or SHOUTcast endpoint — that AudD ingests continuously and recognizes against. Each stream is identified by a radioId you pick.
There are two ways to consume recognition events:
- Callbacks — AudD POSTs each match (and each lifecycle notification) to a URL you control. See Receive callbacks from a stream.
- Longpoll — your code polls AudD's
/longpoll/endpoint and receives matches and notifications synchronously over HTTP. See Poll for stream events (longpoll).
Both modes need a callback URL configured on the account; otherwise events are never delivered. If your consumers will only longpoll — e.g., browser widgets, mobile apps, or scripts that can't expose a public URL — set the account's callback URL to https://audd.tech/empty/ in advance; it's a placeholder URL that discards POSTs. Consumers themselves don't need to configure anything.
Receive callbacks from a stream
The full setup has three steps: configure the callback URL, register a stream, and parse incoming POSTs.
1. Configure the account callback URL
await audd.Streams.SetCallbackUrlAsync(
"https://your-app.example.com/audd-callback",
returnMetadata: new[] { "apple_music", "spotify" }); // optional; populates each callback
returnMetadata is serialized into the URL as ?return=apple_music,spotify (the server reads it from the URL). If you've already baked ?return= into the URL string and also pass returnMetadata, the SDK throws AudDInvalidRequestException rather than silently overwriting.
2. Register the stream
long radioId = 1; // any integer you choose — your handle for this stream
await audd.Streams.AddAsync(
url: "https://radio.example.com/stream.mp3",
radioId: radioId);
url accepts direct stream URLs (DASH, Icecast, HLS, m3u, m3u8) and shortcuts: twitch:<channel>, youtube:<video_id>, youtube-ch:<channel_id>. Pass callbacks: "before" to deliver callbacks at song start instead of song end.
Other stream methods:
await audd.Streams.SetUrlAsync(radioId: radioId, url: "https://radio.example.com/new.mp3");
await audd.Streams.DeleteAsync(radioId: radioId);
IReadOnlyList<Stream> streams = await audd.Streams.ListAsync();
Stream carries RadioId, Url, StreamRunning, LongpollCategory.
3. Parse incoming POST bodies — ASP.NET Minimal API
Each callback POST body is JSON with either a result block (recognition) or a notification block (lifecycle event). Streams.HandleCallbackAsync(stream) reads from any Stream; Streams.ParseCallback(body) is a pure function for JsonElement or string (no I/O) — useful for queue replay or webhook proxies. The result is a CallbackEvent discriminated union — pattern-match on CallbackEvent.Match and CallbackEvent.Notification.
using AudD;
using AudD.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAudD(opts => builder.Configuration.GetSection("AudD").Bind(opts));
var app = builder.Build();
app.MapPost("/audd-callback", async (HttpRequest req, AudD.AudD audd) =>
{
var ev = await audd.Streams.HandleCallbackAsync(req.Body);
switch (ev)
{
case CallbackEvent.Match m:
var song = m.Value.Song;
Console.WriteLine($"{song.Artist} — {song.Title} (radio={m.Value.RadioId}, score={song.Score})");
// persist, queue, fan-out, etc.
break;
case CallbackEvent.Notification n:
// Stream lifecycle: "stream stopped", "can't connect", etc.
Console.WriteLine($"#{n.Value.NotificationCode}: {n.Value.NotificationMessage}");
break;
}
return Results.Ok(new { ok = "true" });
});
app.Run();
A StreamCallbackMatch carries RadioId, Timestamp, PlayLength, Song (the top match), Alternatives (rare extra candidates — may have different artist/title from the top match), and RawResponse (the full unparsed body). A StreamCallbackSong mirrors a RecognitionResult's metadata: Artist, Title, Score, Album, ReleaseDate, Label, SongLink, plus optional provider blocks when returnMetadata was set.
A StreamCallbackNotification carries RadioId, StreamRunning, NotificationCode, NotificationMessage, and Time.
Other framework patterns (MVC controllers, Blazor) — see the GitHub README.
Poll for stream events (longpoll)
For consumers that can't expose a public callback URL — desktop apps, ASP.NET workers behind NAT, .NET MAUI clients, scripts on developer machines — call Streams.LongpollAsync(radioId) and await foreach matches. The SDK runs a background fetch loop and surfaces events as IAsyncEnumerable<T>.
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.LongpollAsync(...) call and throws AudDInvalidRequestException 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:
await audd.Streams.SetCallbackUrlAsync("https://audd.tech/empty/");
long radioId = 1; // your handle for the stream you registered
await using var poll = await audd.Streams.LongpollAsync(radioId, timeout: 30);
await foreach (var match in poll.Matches.WithCancellation(cancellationToken))
{
Console.WriteLine($"{match.Song.Artist} — {match.Song.Title}");
}
The returned LongpollPoll exposes three IAsyncEnumerable<T> streams: Matches, Notifications, and Errors. Errors is single-shot — the first error terminates the subscription, after which Matches and Notifications complete. Keepalive payloads are absorbed silently. Disposing the handle (await using) stops the background fetch.
To consume all three concurrently, fan out with Task.WhenAll:
await using var poll = await audd.Streams.LongpollAsync(radioId, timeout: 30);
var matches = Task.Run(async () =>
{
await foreach (var m in poll.Matches)
Console.WriteLine($"{m.Song.Artist} — {m.Song.Title}");
});
var notifications = Task.Run(async () =>
{
await foreach (var n in poll.Notifications)
Console.WriteLine($"#{n.NotificationCode}: {n.NotificationMessage}");
});
var errors = Task.Run(async () =>
{
await foreach (var err in poll.Errors)
Console.Error.WriteLine($"terminal: {err}");
});
await Task.WhenAll(matches, notifications, errors);
In ASP.NET Core, register AudD as a singleton (see ASP.NET Core / Generic Host integration) and run the longpoll loop from a BackgroundService. Pass the host's stoppingToken into LongpollAsync(...) and WithCancellation(stoppingToken) so shutdown cleanly tears down the subscription.
Tokenless consumers
For Blazor WebAssembly apps, .NET MAUI clients, or any process that should not hold the api_token, the server derives an opaque per-stream identifier and ships only that string to the client. The client subscribes with LongpollConsumer, which never sees or transmits the api_token.
On the server (has the api_token):
string longpollKey = audd.Streams.DeriveLongpollCategory(radioId);
// ship longpollKey to the client over your own auth'd channel
On the client (no api_token):
using var consumer = new LongpollConsumer(category: longpollKey);
await using var poll = consumer.Iterate(timeout: 30);
await foreach (var match in poll.Matches.WithCancellation(cancellationToken))
{
Console.WriteLine($"{match.Song.Artist} — {match.Song.Title}");
}
LongpollConsumer accepts an optional HttpClient, maxRetries, and backoffFactor. The server is responsible for making sure a callback URL is configured on the account before clients start subscribing.
Add a song to your custom catalog
The custom-catalog endpoint adds songs to a private fingerprint database for your account. After upload, future RecognizeAsync calls on the same account can match against your tracks. It is not how you submit audio for music recognition — for that, use RecognizeAsync (or RecognizeEnterpriseAsync).
The custom-catalog endpoint requires special access. Contact api@audd.io to enable it. Calls without access throw AudDCustomCatalogAccessException.
long audioId = 1; // any integer you choose — your reference to the song
await audd.CustomCatalog.AddAsync(audioId: audioId, "https://my.cdn/song.mp3");
The SDK exposes only AddAsync — there is no public list or delete. Track audioId ↔ song mappings yourself. Re-using an audioId re-fingerprints that slot. The same source forms apply (URL string, FileInfo, byte[], Stream).
Handle errors
Every error raised by the SDK subclasses AudDException. Server-reported errors subclass AudDApiException and carry ErrorCode, ServerMessage, HttpStatus, RequestId, RequestedParams, RequestMethod, BrandedMessage, and RawResponse. Network failures throw AudDConnectionException; malformed JSON throws AudDSerializationException.
Idiomatic error handling
using AudD;
await using var audd = new AudD("your-api-token");
try
{
var result = await audd.RecognizeAsync("/path/to/clip.mp3");
}
catch (AudDAuthenticationException e)
{
throw new InvalidOperationException($"check your token: [#{e.ErrorCode}] {e.ServerMessage}", e);
}
catch (AudDQuotaException e)
{
Console.Error.WriteLine($"out of quota: {e.ServerMessage}");
}
catch (AudDRateLimitException e)
{
Console.Error.WriteLine($"rate limited: {e.ServerMessage}");
}
catch (AudDInvalidAudioException e)
{
Console.Error.WriteLine($"audio rejected: {e.ServerMessage}");
}
catch (AudDCustomCatalogAccessException e)
{
Console.Error.WriteLine($"custom catalog: {e.ServerMessage}");
}
catch (AudDApiException e)
{
Console.Error.WriteLine($"AudD #{e.ErrorCode}: {e.ServerMessage} (request_id={e.RequestId})");
}
catch (AudDConnectionException e)
{
Console.Error.WriteLine($"network: {e.Message}");
}
For some codes — 19 (request_blocked, abuse, test_request_scope), 31337 (ip_ban), 903 (token_disabled) — the server populates branded human-readable text alongside the error. The SDK extracts that onto the exception's BrandedMessage property rather than surfacing it as a fake recognition match:
try
{
await audd.RecognizeAsync(source);
}
catch (AudDBlockedException e)
{
Console.Error.WriteLine($"#{e.ErrorCode} {e.ServerMessage}");
if (e.BrandedMessage is not null)
Console.Error.WriteLine($"server brand text: {e.BrandedMessage}");
}
To handle a code the SDK doesn't yet map to a typed subclass, register a custom factory:
AudDErrorMap.Register(1234, (code, msg, http, rid, rp, rm, bm, rr) =>
new AudDServerException(code, msg, http, rid, rp, rm, bm, rr));
Retry behavior
Each endpoint is classified into one of three retry classes:
- Read (
Streams.ListAsync,Streams.GetCallbackUrlAsync, longpolls) — retries on anyHttpRequestExceptionand HTTP 5xx. - Recognition (
RecognizeAsync,RecognizeEnterpriseAsync,Advanced.*) — retries only on pre-upload transport errors to avoid double-billing for in-progress uploads, plus 5xx. - Mutating (
Streams.AddAsync,Streams.SetUrlAsync,Streams.DeleteAsync,Streams.SetCallbackUrlAsync,CustomCatalog.AddAsync) — retries only on pre-upload errors; 5xx is surfaced because the side effect may have already happened.
Defaults: maxRetries: 3, backoffFactor: 0.5. Backoff is min(backoffFactor * 2^attempt, 30 seconds) with 0.5x..1.5x jitter. Override per client:
await using var audd = new AudD("your-api-token", maxRetries: 5, backoffFactor: 1.0);
await using var audd = new AudD("your-api-token", maxRetries: 1); // disable retries
Configuration
Timeouts
Defaults: standard endpoint 60 s read; enterprise endpoint 1 hour read (long files take that long to fingerprint server-side). Override per call with the timeout: argument:
await audd.RecognizeAsync("https://audd.tech/example.mp3", timeout: TimeSpan.FromSeconds(10));
The argument is layered on top of the caller's cancellationToken via CancellationTokenSource.CreateLinkedTokenSource + CancelAfter. For finer-grained control, inject a custom HttpClient.
Custom HttpClient
Inject your own HttpClient for proxies, custom transports, custom TLS, or HTTP/2:
using System.Net;
using AudD;
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://corp-proxy:8080"),
UseProxy = true,
};
var http = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(2) };
await using var audd = new AudD(
apiToken: "your-api-token",
httpClient: http);
enterpriseHttpClient: is a separate optional injection for the enterprise endpoint. The SDK adds its User-Agent only when the injected client doesn't already define one, and does not dispose injected clients on Dispose.
ASP.NET Core / Generic Host integration
Register AudD as a singleton via the DI extension:
using AudD.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAudD(opts => builder.Configuration.GetSection("AudD").Bind(opts));
var app = builder.Build();
Bound appsettings.json:
{
"AudD": {
"ApiToken": "your-api-token",
"MaxRetries": 3,
"BackoffFactor": 0.5
}
}
AddAudD auto-discovers IHttpClientFactory and applies it; configure named/typed clients normally:
builder.Services.AddHttpClient("audd-standard")
.AddPolicyHandler(retryPolicy);
builder.Services.AddAudD(opts =>
{
opts.ApiToken = builder.Configuration["AudD:ApiToken"];
opts.HttpClientName = "audd-standard";
});
AudDOptions properties: ApiToken, MaxRetries ([Range(1, 10)]), BackoffFactor ([Range(0.0, 60.0)]), HttpClientName, EnterpriseHttpClientName.
Observability
Pass an onEvent hook (constructor or AddAudD(...).WithOnEvent(...)) for per-request telemetry:
await using var audd = new AudD(
apiToken: "your-api-token",
onEvent: evt =>
{
if (evt.Kind == AudDEventKind.Response)
Console.WriteLine($"{evt.Method} -> {evt.HttpStatus} in {evt.Elapsed?.TotalMilliseconds:F0}ms");
});
AudDEvent is a record with Kind (Request, Response, Exception), Method, Url, RequestId, HttpStatus, Elapsed, ErrorCode, and a free-form Extras dictionary. Events never carry the api_token or request body. Hook exceptions are caught and logged at debug-level via ILogger<AudD> so observability cannot break the request path.
WithOnEvent has an Action<IServiceProvider, AudDEvent> overload for hooks that need DI-resolved ILoggers, meters, or activity sources.
Token rotation
Covered under Authentication — audd.SetApiToken("new-token") swaps atomically without aborting in-flight requests.
Retries
Covered under Retry behavior. maxRetries: and backoffFactor: on the constructor; same names on AudDOptions.
Lifecycle and concurrency
AudD implements IDisposable and IAsyncDisposable. The recommended pattern is await using (the public surface is async-only):
await using var audd = new AudD("your-api-token");
var result = await audd.RecognizeAsync("https://audd.tech/example.mp3");
Synchronous using is supported as a convenience. Disposal closes only the SDK-owned HttpClient instances; injected clients are untouched. A single AudD instance is safe to share across threads and tasks — DI registers it as a singleton.
Cancelling a RecognizeAsync mid-flight might still consume a request on the server — the metered work may already be in progress by the time the cancel signal lands.
Calling undocumented endpoints
For AudD endpoints not yet wrapped by typed methods on this SDK, hit them by name through audd.Advanced.RawRequestAsync(method, params). It returns the raw JSON body as a JsonElement, runs through the same retry policy, and throws AudDSerializationException if the response isn't a JSON object. This is the supported path for anything beta or one-off — typed wrappers ship as features stabilize.
var body = await audd.Advanced.RawRequestAsync(
"getLinks",
new Dictionary<string, string> { ["audd_id"] = "12345" });