# audd-rust


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-rust" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://crates.io/crates/audd" target="_blank" rel="noopener">crates.io</a>
  <a className="button button--link" href="https://docs.rs/audd" target="_blank" rel="noopener">docs.rs</a>
</div>

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

```toml
[dependencies]
audd = "1.5"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

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

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

```rust
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 5–25-second clip and returns `Some(RecognitionResult)` on a match, or `Ok(None)` when the clip processed but matched nothing.

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

```rust
# use audd::AudD;
# use std::time::Duration;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let result = audd
    .recognize_with(
        "https://audd.tech/example.mp3",
        Some(&["apple_music".into(), "spotify".into()]),
        Some("US"),
        Some(Duration::from_secs(10)),
    )
    .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](https://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 broadcasts, podcasts, and full sets longer than 25 seconds, use the enterprise endpoint. It chunks the file server-side and returns every match.

```rust
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_`, `skip`, `every`, `skip_first_seconds`, `use_timecode`, `accurate_offsets`, `timeout`. Default read/write timeout is one hour.

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

:::

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_` to populate provider sub-objects on the result. Without it, those fields are `None`.

```rust
# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let return_ = ["apple_music".into(), "spotify".into()];
let result = audd
    .recognize_with("https://audd.tech/example.mp3", Some(&return_), None, None)
    .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`, `napster`, `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](https://github.com/AudDMusic/audd-rust#what-you-get-back). Server-side fields not yet typed surface via each model's `extras: HashMap<String, Value>`.

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

- **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

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

```rust
# 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`:

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

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

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

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

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

```rust
audd.streams().set_callback_url("https://audd.tech/empty/", None).await?;
```

:::

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

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

:::warning

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

:::

```rust
# 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`:

| Variant | When 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:

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

```rust
# 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:

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

| Flag | Default | Effect |
|---|---|---|
| `rustls-tls` | yes | TLS via [`rustls`](https://github.com/rustls/rustls) with the Mozilla CA bundle. Pure-Rust, no system dependencies. |
| `native-tls` | no | Platform-native TLS — OpenSSL on Linux, SecureTransport on macOS, SChannel on Windows. Useful for custom corporate CA trust stores or OpenSSL FIPS. |
| `vendored-openssl` | no | OpenSSL 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`:

```toml
[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`:

```rust
# use audd::AudD;
# use std::time::Duration;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
audd.recognize_with(
    "https://audd.tech/example.mp3",
    None,
    None,
    Some(Duration::from_secs(10)),
)
.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:

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

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

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

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

:::warning 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 undocumented endpoints

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.

```rust
# 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](https://dashboard.audd.io) · [HTTP API reference](/) · [Other SDKs](/sdks)
