# audd-dotnet


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-dotnet" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://www.nuget.org/packages/AudD/" target="_blank" rel="noopener">NuGet</a>
</div>

Recognize music in audio clips, long broadcasts, and live streams from .NET / C#.

```bash
dotnet add package AudD
```

```csharp
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](https://dashboard.audd.io).

The api_token is resolved on construction in this order:

1. The `apiToken` constructor argument.
2. The `AUDD_API_TOKEN` environment variable.
3. Otherwise, `ArgumentException` with a pointer to [dashboard.audd.io](https://dashboard.audd.io).

```csharp
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:

```csharp
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.

```csharp
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](https://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.

```csharp
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`).

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

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`.

```csharp
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:

```csharp
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](https://github.com/AudDMusic/audd-dotnet#what-you-get-back).

## 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](#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)](#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

```csharp
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

```csharp
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:

```csharp
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`.

```csharp
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](https://github.com/AudDMusic/audd-dotnet#streams).

## 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>`.

:::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.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:

```csharp
await audd.Streams.SetCallbackUrlAsync("https://audd.tech/empty/");
```

:::

```csharp
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`:

```csharp
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](#aspnet-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):

```csharp
string longpollKey = audd.Streams.DeriveLongpollCategory(radioId);
// ship longpollKey to the client over your own auth'd channel
```

On the client (no api_token):

```csharp
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`).

:::warning

The custom-catalog endpoint requires special access. Contact [api@audd.io](mailto:api@audd.io) to enable it. Calls without access throw `AudDCustomCatalogAccessException`.

:::

```csharp
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

```csharp
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:

```csharp
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:

```csharp
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 any `HttpRequestException` and 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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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`:

```json
{
  "AudD": {
    "ApiToken": "your-api-token",
    "MaxRetries": 3,
    "BackoffFactor": 0.5
  }
}
```

`AddAudD` auto-discovers `IHttpClientFactory` and applies it; configure named/typed clients normally:

```csharp
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:

```csharp
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 `ILogger`s, meters, or activity sources.

### Token rotation

Covered under [Authentication](#authentication) — `audd.SetApiToken("new-token")` swaps atomically without aborting in-flight requests.

### Retries

Covered under [Retry behavior](#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):

```csharp
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.

:::warning Cancellation does not refund server-side metering

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.

```csharp
var body = await audd.Advanced.RawRequestAsync(
    "getLinks",
    new Dictionary<string, string> { ["audd_id"] = "12345" });
```

---

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