Skip to main content

audd-rust

Open .md

Recognize music in audio clips, long broadcasts, and live streams from Rust — async, tokio-based.

[dependencies]
audd = "1.5"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use audd::AudD;

#[tokio::main]
async fn main() -> Result<(), audd::AudDError> {
let audd = AudD::new("test"); // 10 reqs/day; get a real token at dashboard.audd.io
if let Some(r) = audd.recognize("https://audd.tech/example.mp3").await? {
println!("{} — {}", r.artist.as_deref().unwrap_or(""), r.title.as_deref().unwrap_or(""));
}
Ok(())
}

Authentication​

Get your API token at dashboard.audd.io.

The api_token is resolved on construction in this order:

  1. The argument to AudD::new, AudD::try_new, or AudD::builder(...).
  2. The AUDD_API_TOKEN environment variable.
  3. Otherwise AudDError::Configuration, with a pointer to dashboard.audd.io.
use audd::AudD;

let audd = AudD::new("your-api-token"); // explicit; panics on no-token
let audd = AudD::try_new("your-api-token")?; // Result-returning sibling
let audd = AudD::from_env()?; // reads AUDD_API_TOKEN
# Ok::<_, audd::AudDError>(())

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.set_api_token("new-token")?;

set_api_token is thread-safe and tokio-task-safe — it swaps the token under an RwLock shared by the standard and enterprise transports. In-flight requests finish on the previous token; subsequent calls use the new one. audd.api_token() returns the current value.

Recognize a clip​

recognize() takes a short audio clip and returns Some(RecognitionResult) on a match, or Ok(None) when the clip processed but matched nothing.

use audd::{AudD, Source};
use std::path::PathBuf;

# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
// URL — auto-detected from the &str prefix; server fetches the audio.
audd.recognize("https://audd.tech/example.mp3").await?;

// Filesystem path — &str, &Path, or PathBuf.
audd.recognize(PathBuf::from("/clip.mp3")).await?;

// Bytes.
let bytes: Vec<u8> = std::fs::read("/clip.mp3").unwrap();
audd.recognize(bytes).await?;

// Async reader (anything `AsyncRead + Send + Unpin`).
let file = tokio::fs::File::open("/clip.mp3").await.unwrap();
audd.recognize(Source::Reader(Box::new(file))).await?;
# Ok(()) }

recognize is the no-knob default. For provider blocks, region, or per-call timeouts use recognize_with:

# use audd::{AudD, RecognizeOptions};
# use std::time::Duration;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let result = audd
.recognize_with(
"https://audd.tech/example.mp3",
RecognizeOptions {
return_metadata: Some(&["apple_music".into(), "spotify".into()]),
market: Some("US"),
timeout: Some(Duration::from_secs(10)),
..Default::default()
},
)
.await?;
# let _ = result; Ok(()) }

A RecognitionResult carries timecode, artist, title, album, release_date, label, song_link, isrc / upc (Startup plan or higher), provider blocks (when requested), an extras: HashMap<String, Value> for forward-compat, and helpers — thumbnail_url(), streaming_url(provider), streaming_urls(), preview_url(). Full reference: github.com/AudDMusic/audd-rust#what-you-get-back.

Source-form notes: Path re-reads on each retry, Bytes clones, Reader buffers the bytes on first read so retries replay from memory — very large readers may spike memory; prefer Path or Bytes when audio doesn't fit in RAM. A non-existent path surfaces as AudDError::Source before the first network attempt.

Process a long audio file​

For full-length songs, short-form videos, podcasts, broadcasts, and DJ sets, use the enterprise endpoint. It chunks the file server-side and returns every match.

use audd::{AudD, EnterpriseOptions};

# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let matches = audd
.recognize_enterprise(
"/path/to/broadcast.mp3",
EnterpriseOptions { limit: Some(10), ..Default::default() },
)
.await?;

for m in matches {
println!(
"{} {} — {} (score={})",
m.timecode,
m.artist.as_deref().unwrap_or(""),
m.title.as_deref().unwrap_or(""),
m.score,
);
}
# Ok(()) }

EnterpriseOptions derives Default — reach for the struct-update form (EnterpriseOptions { limit: Some(N), ..Default::default() }) and only set what you need. Other fields: return_metadata, skip, every, skip_first_seconds, use_timecode, accurate_offsets, timeout. Default read/write timeout is one hour.

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.

Each EnterpriseMatch carries score, timecode, artist, title, album, release_date, label, song_link, isrc/upc (Startup+), start_offset, end_offset, extras, plus thumbnail_url(), streaming_url(provider), streaming_urls().

Get streaming-service metadata​

Pass return_metadata to populate provider sub-objects on the result. Without it, those fields are None.

# use audd::{AudD, RecognizeOptions};
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let return_metadata = ["apple_music".into(), "spotify".into()];
let result = audd
.recognize_with(
"https://audd.tech/example.mp3",
RecognizeOptions { return_metadata: Some(&return_metadata), ..Default::default() },
)
.await?;

if let Some(r) = result {
if let Some(am) = &r.apple_music {
println!("Apple Music: {:?}", am.url);
}
if let Some(sp) = &r.spotify {
println!("Spotify URI: {:?}", sp.uri);
}

// Or resolve any provider — direct URL when the metadata block is set,
// else the lis.tn redirect when song_link is on lis.tn.
println!("{:?}", r.streaming_url(audd::StreamingProvider::Spotify));
println!("{:?}", r.streaming_urls()); // every resolvable provider
println!("{:?}", r.preview_url()); // 30-second preview, provider terms apply
}
# Ok(()) }

Valid providers: apple_music, spotify, deezer, musicbrainz. Each provider adds latency. Pass market (ISO 3166 region code, e.g. "GB") to recognize_with to control the Apple Music storefront.

For the field shape of each provider sub-object, see the upstream API reference at docs.audd.io and the GitHub README. Server-side fields not yet typed surface via each model's extras: HashMap<String, Value>.

Pass arbitrary form fields​

For form fields the SDK doesn't expose as typed options — parameters the typed options don't cover, account-specific flags — set extra_parameters on the options struct:

# use audd::{AudD, RecognizeOptions};
# use std::collections::HashMap;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let mut extras = HashMap::new();
extras.insert("my_custom_flag".to_string(), "1".to_string());
let result = audd
.recognize_with(
"https://audd.tech/example.mp3",
RecognizeOptions { extra_parameters: Some(&extras), ..Default::default() },
)
.await?;
# let _ = result; Ok(()) }

Typed fields win on collision. Available on RecognizeOptions and EnterpriseOptions.

Monitor a live audio stream​

A stream in AudD is a long-running audio source — a radio URL, HLS, Icecast, SHOUTcast, or a twitch:<channel> / youtube:<video_id> shortcut — that AudD ingests continuously and recognizes against. Each stream is identified by a radio_id: i64 you pick.

There are two ways to consume recognition events:

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​

# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
audd.streams()
.set_callback_url(
"https://your-app.example.com/audd-callback",
Some(&["apple_music".into(), "spotify".into()]), // optional; populates each callback
)
.await?;
# Ok(()) }

return_metadata is serialized into the URL as ?return=apple_music,spotify (the server reads it from the URL). If url already has a ?return= parameter and return_metadata.is_some(), the SDK returns AudDError::Api with ErrorKind::InvalidRequest rather than silently overwriting.

2. Register the stream​

# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let radio_id = 1i64; // any integer you choose — your handle for this stream
audd.streams()
.add("https://radio.example.com/stream.mp3", radio_id, None)
.await?;
# Ok(()) }

Other stream methods — set_url(radio_id, url), delete(radio_id), list() (returns Vec<Stream>), and get_callback_url(). add takes an optional third argument callbacks: Option<&str> — comma-separated extra callback URLs for this stream only, or Some("before") to deliver callbacks at song start instead of song end.

3. Parse incoming POST bodies — axum​

Each callback POST body is JSON with either a result block (recognition) or a notification block (lifecycle event). audd::handle_callback accepts raw bytes and returns a CallbackEvent:

pub enum CallbackEvent {
Match(StreamCallbackMatch),
Notification(StreamCallbackNotification),
}

A canonical axum recipe — pull Bytes from the request and hand them to handle_callback:

use audd::{handle_callback, CallbackEvent};
use axum::{http::StatusCode, routing::post, Router};
use bytes::Bytes;

async fn audd_callback(body: Bytes) -> Result<StatusCode, (StatusCode, String)> {
match handle_callback(&body) {
Ok(CallbackEvent::Match(m)) => {
let song = &m.song;
println!(
"{} — {} (radio={}, score={})",
song.artist, song.title, m.radio_id, song.score,
);
// persist, queue, fan-out, ...
}
Ok(CallbackEvent::Notification(n)) => {
// Stream lifecycle: "stream stopped", "can't connect", etc.
println!("#{}: {}", n.notification_code, n.notification_message);
}
Err(e) => return Err((StatusCode::BAD_REQUEST, format!("malformed: {e}"))),
}
Ok(StatusCode::NO_CONTENT)
}

pub fn app() -> Router {
Router::new().route("/audd-callback", post(audd_callback))
}

handle_callback and parse_callback are exposed as free functions (no AudD instance required), so they work in queue replay tools, webhook proxies, and framework-agnostic tests. Use parse_callback(value: serde_json::Value) when you've already deserialized the body.

A StreamCallbackMatch carries radio_id, timestamp, play_length, song (the top match — StreamCallbackSong with artist, title, score, album, release_date, label, song_link, plus optional provider blocks when return_metadata was set), alternatives (rare extra candidates — may have a different artist or title from the top match), extras, and raw_response (the full unparsed body).

A StreamCallbackNotification carries radio_id, stream_running, notification_code, notification_message, time, extras, and raw_response.

Other frameworks (actix-web, warp, rocket) — see the GitHub README.

Poll for stream events (longpoll)​

For consumers that can't expose a public callback URL — desktop apps, scripts behind NAT, anything without a public HTTPS receiver — call streams().longpoll_by_radio_id(...). It returns a LongpollPoll handle whose three streams — matches, notifications, errors — are driven by a background tokio task. Drive them with tokio::select!.

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 longpoll_by_radio_id(...) call and returns AudDError::Api with ErrorKind::InvalidRequest and guidance if it's missing; pass LongpollOptions::default().skip_callback_check(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:

audd.streams().set_callback_url("https://audd.tech/empty/", None).await?;
use audd::{AudD, LongpollOptions};
use futures_util::StreamExt;

# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let radio_id = 1i64; // any integer you choose — your handle for this stream
let mut poll = audd
.streams()
.longpoll_by_radio_id(radio_id, LongpollOptions::default().timeout(30))
.await?;

loop {
tokio::select! {
biased;
Some(err) = poll.errors.next() => {
eprintln!("longpoll terminated: {err}");
break;
}
Some(n) = poll.notifications.next() => {
println!("notif: {}", n.notification_message);
}
Some(m) = poll.matches.next() => {
println!("matched: {} — {}", m.song.artist, m.song.title);
}
else => break,
}
}
poll.close().await;
# Ok(()) }

matches carries StreamCallbackMatch, notifications carries StreamCallbackNotification (same shapes as the callback receiver above). errors is single-shot — the first terminal error closes matches and notifications too. LongpollOptions builds with Default::default() and chains .timeout(secs), .since_time(unix_ms), .skip_callback_check(true). close().await shuts down deterministically; dropping the handle aborts the background task best-effort.

Tokenless consumers​

For browser widgets, mobile clients, WASM frontends, or any consumer that should not see the api_token: a server with the token derives an opaque per-stream identifier with audd::derive_longpoll_category(api_token, radio_id) and ships only that string to the client. The client subscribes via LongpollConsumer — no api_token ever leaves the server.

use audd::{LongpollConsumer, LongpollIterateOptions};
use futures_util::StreamExt;

# async fn run(category_from_server: String) -> Result<(), audd::AudDError> {
let consumer = LongpollConsumer::new(category_from_server);
let mut poll = consumer.iterate(LongpollIterateOptions::default());
while let Some(m) = poll.matches.next().await {
println!("matched: {} — {}", m.song.artist, m.song.title);
}
poll.close().await;
# Ok(()) }

The server-side caller (which holds the token) is responsible for ensuring a callback URL is configured before handing identifiers out.

Add a song to your custom catalog​

The custom-catalog endpoint adds songs to a private fingerprint database for your account. After upload, future recognize() calls on the same account can match against your tracks. It is not how you submit audio for music recognition — for that, use recognize (or recognize_enterprise).

danger

The custom-catalog endpoint requires special access. Contact api@audd.io to enable it. Calls without access return AudDError::Api with ErrorKind::CustomCatalogAccess.

# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let audio_id = 1i64; // any integer you choose — your reference to the song
audd.custom_catalog().add(audio_id, "https://my.cdn/song.mp3").await?;
# Ok(()) }

The SDK exposes only add — there is no public list or delete. Track audio_id ↔ song mappings yourself. Re-using an audio_id re-fingerprints that slot.

Handle errors​

Every fallible method returns Result<T, AudDError>. AudDError is a single enum derived with thiserror:

VariantWhen raised
Api { code, message, kind, http_status, request_id, requested_params, branded_message, raw_response, .. }Server returned status: error with a parseable body.
Server { http_status, message, .. }Non-2xx HTTP with a non-JSON body (e.g. 502 with HTML from an upstream gateway).
Connection { message, source }Network / TLS / timeout — no response received. Wraps the underlying reqwest::Error via the source chain.
Serialization { message, raw_text }2xx with malformed JSON, or unexpectedly-shaped JSON the typed model couldn't deserialize.
Source(String)Caller misuse: nonexistent path, retry against a consumed unbuffered reader, malformed callback URL.
Configuration { message }Construction-time misconfiguration — no api_token supplied with AUDD_API_TOKEN unset, or set_api_token("").

Idiomatic error handling​

The AudDError::Api variant carries an ErrorKind for semantic dispatch — match on it directly, or use the is_* helpers:

use audd::{AudD, AudDError, ErrorKind};

# async fn run(audd: AudD) -> Result<(), AudDError> {
match audd.recognize("/path/to/clip.mp3").await {
Ok(_) => {}
Err(AudDError::Api { kind: ErrorKind::Authentication, code, message, .. }) => {
eprintln!("check your token: [#{code}] {message}");
}
Err(AudDError::Api { kind: ErrorKind::Quota, message, .. }) => {
eprintln!("out of quota: {message}");
}
Err(AudDError::Api { kind: ErrorKind::RateLimit, message, .. }) => {
eprintln!("rate limited: {message}");
}
Err(AudDError::Api { kind: ErrorKind::InvalidAudio, message, .. }) => {
eprintln!("audio rejected: {message}");
}
Err(AudDError::Api { kind: ErrorKind::CustomCatalogAccess, .. }) => {
eprintln!("contact api@audd.io for custom-catalog access");
}
Err(AudDError::Connection { message, .. }) => {
eprintln!("network: {message}");
}
Err(other) => return Err(other),
}
# Ok(()) }

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 AudDError::Api { branded_message, .. } rather than surfacing it as a fake recognition match:

# use audd::{AudD, AudDError, ErrorKind};
# async fn run(audd: AudD) -> Result<(), AudDError> {
match audd.recognize("https://audd.tech/example.mp3").await {
Ok(_) => {}
Err(AudDError::Api { kind: ErrorKind::Blocked, code, message, branded_message: Some(b), .. }) => {
eprintln!("[#{code}] {message}");
eprintln!("server brand text: {b}");
}
Err(other) => return Err(other),
}
# Ok(()) }

When the server returns code 51 (deprecated parameter) with a usable result, the SDK emits a tracing::warn! on the audd target and returns the result as if the call had succeeded. Code 51 with no result raises AudDError::Api with ErrorKind::InvalidRequest.

Retry behavior​

Each endpoint is classified into one of three retry classes:

  • Read (streams.list, streams.get_callback_url, longpoll polls) — retries on any reqwest::Error, plus HTTP 408/429/5xx.
  • Recognition (recognize, recognize_enterprise, advanced.*) — retries only on pre-upload network errors (connect, DNS, TLS) to avoid double-billing for in-progress uploads, plus 5xx (skips 429 — cost concern).
  • Mutating (streams.set_callback_url, streams.add, streams.set_url, streams.delete, custom_catalog.add) — retries only on pre-upload errors; 5xx is surfaced because the side effect may have already happened.

Defaults: max_attempts = 3, backoff_factor = 0.5, backoff_max = 30.0. Backoff is min(backoff_factor * 2^attempt, backoff_max) with 0.5x..1.5x deterministic jitter. Override per client:

let audd = AudD::builder("your-api-token")
.max_attempts(5)
.backoff_factor(1.0)
.build()?;

let audd = AudD::builder("your-api-token").max_attempts(1).build()?; // disable retries
# Ok::<_, audd::AudDError>(())

Configuration​

TLS feature flags​

Pick exactly one TLS backend at compile time:

FlagDefaultEffect
rustls-tlsyesTLS via rustls with the Mozilla CA bundle. Pure-Rust, no system dependencies.
native-tlsnoPlatform-native TLS — OpenSSL on Linux, SecureTransport on macOS, SChannel on Windows. Useful for custom corporate CA trust stores or OpenSSL FIPS.
vendored-opensslnoOpenSSL via native-tls, statically linked from a vendored source build. Useful when the build host lacks libssl-dev / openssl-devel.

To swap the default rustls for native-tls:

[dependencies]
audd = { version = "1.5", default-features = false, features = ["native-tls"] }

MSRV is Rust 1.88.

Timeouts​

Defaults: standard endpoint 30 s connect / 60 s read & write; enterprise endpoint 30 s connect / 3600 s read & write because long files take that long to fingerprint server-side. Override per call on recognize_with / recognize_enterprise:

# use audd::{AudD, RecognizeOptions};
# use std::time::Duration;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
audd.recognize_with(
"https://audd.tech/example.mp3",
RecognizeOptions { timeout: Some(Duration::from_secs(10)), ..Default::default() },
)
.await?;
# Ok(()) }

You can also wrap any call with tokio::time::timeout(...) for cooperative cancellation.

Custom HTTP client​

Inject your own reqwest::Client for proxies, custom transports, custom TLS, or HTTP/2:

use audd::AudD;

# fn run() -> Result<(), Box<dyn std::error::Error>> {
let http = reqwest::Client::builder()
.proxy(reqwest::Proxy::all("http://corp-proxy:8080")?)
.build()?;

let audd = AudD::builder("your-api-token")
.reqwest_client(http)
.build()?;
# let _ = audd; Ok(()) }

The injected client is shared by both standard and enterprise endpoints — the SDK does not override its timeouts. If your standard timeouts conflict with enterprise's hour-long reads, omit the injection and let the SDK build them.

Observability​

The SDK logs through the audd tracing target — install any subscriber to capture deprecation warnings and retry diagnostics:

tracing_subscriber::fmt().with_env_filter("audd=info").init();

For structured per-request observability, register an on_event hook on the builder:

use std::sync::Arc;
use audd::{AudD, AudDEvent, OnEventHook};

let hook: OnEventHook = Arc::new(|event: &AudDEvent| {
eprintln!(
"audd {} {} -> {:?} ({}ms)",
event.method,
event.url,
event.http_status,
event.elapsed.as_millis(),
);
});

let audd = AudD::builder("your-api-token").on_event(hook).build()?;
# Ok::<_, audd::AudDError>(())

AudDEvent carries kind (Request / Response / Exception), method, url, request_id, http_status, elapsed, error_code, and a free-form extras. Events never carry the api_token or request/response bodies. Hook panics are caught and suppressed (std::panic::catch_unwind) so observability can never break the request path.

Concurrency and lifecycle​

AudD is Send + Sync + 'static and Clone — clones share the inner reqwest::Client connection pool and the Arc<RwLock<String>> token. Share one instance across tokio tasks; spawn freely. Drop the value (or call audd.close().await for parity with sibling SDKs) when you're done.

Cancellation does not refund server-side metering

tokio::time::timeout and tokio::select! work as expected, but cancelling a recognize() 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 endpoints the SDK doesn't wrap​

For AudD endpoints not yet wrapped by typed methods on this SDK, hit them by name through audd.advanced().raw_request(method, params). It returns the raw JSON body as a serde_json::Value, runs through the same retry policy, and is the supported path for anything beta or one-off — typed wrappers ship as features stabilize. raw_request_strict raises AudDError::Api on status: error bodies; plain raw_request returns the body for inspection.

# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let body = audd
.advanced()
.raw_request("getLinks", &[("audd_id", "12345".to_string())])
.await?;
# let _ = body; Ok(()) }

dashboard.audd.io · HTTP API reference · Other SDKs