# audd-cpp


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-cpp" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://github.com/AudDMusic/audd-cpp/releases" target="_blank" rel="noopener">Releases</a>
</div>

Recognize music in audio clips, long broadcasts, and live streams from C++.

```cmake
include(FetchContent)
FetchContent_Declare(audd
    GIT_REPOSITORY https://github.com/AudDMusic/audd-cpp.git
    GIT_TAG        v1.5.7
)
FetchContent_MakeAvailable(audd)
target_link_libraries(your_app PRIVATE audd::audd)
```

```cpp
#include <audd/audd.hpp>
#include <iostream>

int main() {
    // 10 reqs/day; get a real token at https://dashboard.audd.io
    audd::AudD client("test");

    auto song = client.recognize("https://audd.tech/example.mp3");
    if (song) std::cout << song->artist << " — " << song->title << "\n";
}
```

C++17 by default; pass `-DAUDD_CXX20=ON` to compile under C++20. RAII —
no explicit free; `audd::AudD` owns internal state and releases it when
it goes out of scope.

## Authentication

Get your API token at [dashboard.audd.io](https://dashboard.audd.io).

`audd::AudD(token)` resolves the API token in this order:

1. The `token` constructor argument, when non-empty.
2. The `AUDD_API_TOKEN` environment variable.
3. Otherwise the client is still constructed; the first network call throws
   `audd::AudDApiError` with `error_code = 901`.

```cpp
audd::AudD client("your-api-token");  // explicit
audd::AudD client_env("");            // reads AUDD_API_TOKEN
```

To fail fast at construction time, call `audd::AudD::strict(...)` — it
throws `audd::AudDMissingApiTokenError` when neither the argument nor the
env var resolves to a non-empty token, and returns a movable
`std::unique_ptr<AudD>`:

```cpp
auto client = audd::AudD::strict("");  // throws if AUDD_API_TOKEN is unset
```

The public `"test"` token is capped at 10 requests/day on the standard
endpoint only — fine for hello-worlds, not enough for production.

For long-running services that pull tokens from a secret manager, rotate
without rebuilding the client:

```cpp
client.set_api_token("new-token");
```

The swap is atomic with respect to in-flight requests: in-flight calls
finish under the previous token; subsequent calls use the new value.
`client.api_token()` returns the in-effect token (empty when none set).

## Recognize a clip

`recognize` takes a 5–25-second clip — URL, file path, or raw bytes — and
returns `std::optional<RecognitionResult>`. `std::nullopt` means the clip
processed but matched nothing; an exception means something went wrong.

```cpp
auto song = client.recognize("https://audd.tech/example.mp3");
// or: client.recognize("/path/to/clip.mp3");

if (song) {
    std::cout << song->artist << " — " << song->title
              << " @ " << song->timecode << "\n";
    std::cout << "song page: " << song->song_link << "\n";
    std::cout << "cover art: " << song->thumbnail_url() << "\n";
}
```

A bare `std::string` is auto-classified: an `http://` / `https://` prefix is
sent as a URL, anything else is opened as a file path. To force the
classification (or for in-memory bytes), use the typed `Source` forms:

```cpp
client.recognize(audd::SourceUrl{"https://audd.tech/example.mp3"});
client.recognize(audd::SourceFilePath{"/clip.mp3"});

std::vector<std::uint8_t> bytes = read_buffer();
client.recognize(audd::SourceBytes{
    .bytes = std::move(bytes),
    .name  = "clip.mp3",
    .mime_type = "audio/mpeg",
});
```

For non-blocking call sites, every blocking method has an `_async` twin
returning `std::future<T>`:

```cpp
auto fut = client.recognize_async("https://audd.tech/example.mp3");
auto song = fut.get();
```

A `RecognitionResult` carries `artist`, `title`, `album`, `release_date`,
`label`, `timecode`, `song_link`, `isrc` / `upc` (enterprise plans), plus
helpers — `thumbnail_url()`, `streaming_url(provider)`, `streaming_urls()`,
`preview_url()`. Server fields not yet typed surface via `result->extras`
(an `std::map<std::string, nlohmann::json>`). Full reference:
[github.com/AudDMusic/audd-cpp#what-you-get-back](https://github.com/AudDMusic/audd-cpp#what-you-get-back).

Custom-catalog matches populate `audio_id` instead of artist / title — use
`is_custom_match()` / `is_public_match()` to discriminate.

## Process a long audio file

For broadcasts, podcasts, and full sets longer than 25 seconds, use
`recognize_enterprise`. AudD chunks the file server-side and returns every
match.

```cpp
audd::EnterpriseOptions opts;
opts.limit = 10;  // ALWAYS set this in development

auto matches = client.recognize_enterprise("/path/to/broadcast.mp3", opts);
for (const auto& m : matches) {
    std::cout << m.timecode << "  "
              << m.artist << " — " << m.title
              << "  (score=" << m.score << ")\n";
}
```

Other knobs on `EnterpriseOptions`: `skip`, `every`, `skip_first_seconds`,
`use_timecode`, `accurate_offsets`, `return_metadata`. All numeric fields
are `std::optional<int>` — leave them unset to use the server defaults.
The endpoint accepts the same `Source` forms as `recognize`.

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

Always set `opts.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` (enterprise
plans), `start_offset`, `end_offset`, plus `thumbnail_url()`,
`streaming_url(provider)`, `streaming_urls()`. The `_async` twin
(`recognize_enterprise_async`) returns a
`std::future<std::vector<EnterpriseMatch>>`.

## Get streaming-service metadata

Set `RecognizeOptions::return_metadata` to populate provider sub-objects on
the result. Without it, those `std::optional` blocks are empty.

```cpp
audd::RecognizeOptions opts;
opts.return_metadata = {"apple_music", "spotify"};

auto song = client.recognize("https://audd.tech/example.mp3", opts);

if (song) {
    if (song->apple_music)
        std::cout << "Apple Music: " << song->apple_music->url << "\n";
    if (song->spotify)
        std::cout << "Spotify URI: " << song->spotify->uri << "\n";

    // Or resolve any provider — direct URL when the metadata block is
    // populated, else the lis.tn redirect when song_link is on lis.tn.
    std::cout << song->streaming_url(audd::StreamingProvider::Spotify) << "\n";
    std::cout << song->preview_url() << "\n";
}
```

Valid providers (the strings in `return_metadata`): `apple_music`,
`spotify`, `deezer`, `napster`, `musicbrainz`. Each provider adds latency.
The `audd::StreamingProvider` enum (used by `streaming_url`) covers
`Spotify`, `AppleMusic`, `Deezer`, `Napster`, `YouTube`.

The `market` option (ISO 3166 region code) controls the Apple Music
storefront:

```cpp
audd::RecognizeOptions opts;
opts.return_metadata = {"apple_music"};
opts.market = "GB";
```

For each provider sub-object's full field shape, see
[the upstream API reference](/) and
[the GitHub README](https://github.com/AudDMusic/audd-cpp#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 `radio_id` 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 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

Setup is three steps: configure the callback URL, register the stream,
and parse incoming POSTs.

### 1. Configure the account callback URL

```cpp
audd::SetCallbackUrlOptions copts;
copts.return_metadata = {"apple_music", "spotify"};

client.streams().set_callback_url(
    "https://your-app.example.com/audd-callback", copts);
```

`return_metadata` 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 `return_metadata`, the SDK throws rather than silently overwriting.

### 2. Register the stream

```cpp
int radio_id = 1;             // any integer you choose — your handle for this stream

audd::AddStreamRequest req;
req.url      = "https://radio.example.com/stream.mp3";
req.radio_id = radio_id;
// req.callbacks = "before";  // fire callbacks at song start (default: end)

client.streams().add(req);
```

`url` accepts direct stream URLs (DASH, Icecast, HLS, m3u/m3u8) and
shortcuts: `twitch:<channel>`, `youtube:<video_id>`,
`youtube-ch:<channel_id>`.

Other stream-management methods:

```cpp
int radio_id = 1; // your stream's handle from when you called streams().add(...)

client.streams().set_url(radio_id, "https://radio.example.com/new.mp3");
client.streams().del(radio_id);

for (const auto& s : client.streams().list()) {
    std::cout << s.radio_id << " " << s.url
              << " running=" << s.stream_running << "\n";
}
```

### 3. Parse incoming POST bodies

`audd::handle_callback(body)` and `audd::parse_callback(body)` are
transport-agnostic: feed them the raw POST body from any HTTP framework.
Both return a `CallbackEvent` — a `std::variant<StreamCallbackMatch,
StreamCallbackNotification>` — and throw `audd::AudDSerializationError`
on malformed input.

#### cpp-httplib — canonical recipe

cpp-httplib ships vendored under `vendor/cpp-httplib/` — link the headers
in (no extra dependency) and write the receiver in a few lines:

```cpp
#include <httplib.h>
#include <audd/audd.hpp>
#include <iostream>

int main() {
    httplib::Server server;

    server.Post("/audd-callback",
        [](const httplib::Request& req, httplib::Response& res) {
            try {
                auto ev = audd::handle_callback(req.body);
                std::visit([&](auto&& v) {
                    using T = std::decay_t<decltype(v)>;
                    if constexpr (std::is_same_v<T, audd::StreamCallbackMatch>) {
                        std::cout << "matched on radio_id=" << v.radio_id
                                  << ": " << v.song.artist
                                  << " — " << v.song.title << "\n";
                    } else if constexpr (std::is_same_v<T,
                                            audd::StreamCallbackNotification>) {
                        std::cout << "notification #" << v.notification_code
                                  << ": " << v.notification_message << "\n";
                    }
                }, ev);
                res.status = 200;
            } catch (const audd::AudDError& e) {
                res.status = 400;
                res.set_content(e.what(), "text/plain");
            }
        });

    server.listen("0.0.0.0", 8080);
}
```

If you prefer not to use `std::visit`, the `is_match` /
`is_notification` / `match_or_null` / `notification_or_null` helpers
work just as well:

```cpp
auto ev = audd::handle_callback(req.body);
if (auto* m = audd::match_or_null(ev)) {
    std::cout << m->song.artist << " — " << m->song.title << "\n";
} else if (auto* n = audd::notification_or_null(ev)) {
    std::cout << n->notification_message << "\n";
}
```

A `StreamCallbackMatch` carries `radio_id`, `timestamp`, `play_length`,
`song` (the top match), `alternatives` (rare extra candidates — **may
have different artist/title** from the top match), and `raw_response`. A
`StreamCallbackSong` mirrors a recognition's metadata. A
`StreamCallbackNotification` carries `radio_id`, `stream_running`,
`notification_code`, `notification_message`, and `time`.

Other servers (Crow, Drogon, Pistache, Boost.Beast) — see
[the GitHub README](https://github.com/AudDMusic/audd-cpp#streams).

## Poll for stream events (longpoll)

For consumers that can't expose a public callback URL — desktop apps,
scripts behind NAT, embedded clients — call
`client.streams().longpoll(radio_id)` instead. The SDK opens the
subscription, drives HTTP I/O on a background thread, and surfaces
matches, notifications, and a single terminal error to your code.

:::warning Longpoll requires a configured callback URL

Unless a callback URL is configured for the account, longpoll requests
just return keepalives — no events ever fire. The SDK preflights this
on your first `streams().longpoll(...)` call and throws
`audd::AudDApiError` (category `InvalidRequest`) with guidance if it's
missing; set `opts.skip_callback_check = true` once you've configured
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:

```cpp
client.streams().set_callback_url("https://audd.tech/empty/");
```

:::

Three consumption patterns share the same `LongpollPoll` object. Pick
whichever fits your event loop.

**Callback-style** — `run` blocks until a terminal error fires:

```cpp
int radio_id = 1; // any integer you choose — your handle for this stream

auto poll = client.streams().longpoll(radio_id);
poll.run(
    [](audd::StreamCallbackMatch m) {
        std::cout << m.song.artist << " — " << m.song.title << "\n";
    },
    [](audd::StreamCallbackNotification n) {
        std::cout << "notification: " << n.notification_message << "\n";
    },
    [](std::exception_ptr ep) {
        try { std::rethrow_exception(ep); }
        catch (const std::exception& e) {
            std::cerr << "terminal: " << e.what() << "\n";
        }
    });
```

**Blocking pull** — call `next_match()` / `next_notification()` /
`next_error()` from your own loop. Each returns `std::optional<T>`;
`std::nullopt` means the stream has terminated.

```cpp
auto poll = client.streams().longpoll(radio_id);
while (auto m = poll.next_match()) {
    std::cout << m->song.artist << " — " << m->song.title << "\n";
}
```

**Future-based** — `next_match_async()` / `next_notification_async()` /
`next_error_async()` each return a `std::future<std::optional<T>>` that
resolves on the worker thread.

`LongpollOptions` exposes `since_time` (Unix timestamp to resume from;
0 = "from now"), `timeout_seconds` (server-side wait; default 50), and
`skip_callback_check` (bypass the preflight described above).

`LongpollPoll` is move-only. Letting it go out of scope cleanly joins
the worker thread; `poll.close()` is idempotent and safe from any
thread.

### Tokenless consumers

When the consumer must not hold the api_token — a native desktop app,
mobile build, or embedded device shipped to end users — keep the token
on a server you control and ship the consumer an opaque per-stream
identifier instead.

On the server, with the api_token in hand:

```cpp
std::string subscription =
    audd::derive_longpoll_category("your-api-token", radio_id);
// send `subscription` to the consumer over your own channel
```

On the consumer, with no api_token:

```cpp
audd::AudD client("");  // no token needed for this overload
auto poll = client.streams().longpoll(subscription);
while (auto m = poll.next_match()) {
    std::cout << m->song.artist << " — " << m->song.title << "\n";
}
```

The `longpoll(std::string)` overload accepts the identifier directly
and reuses the same three consumption patterns above.

## 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
throw `audd::AudDCustomCatalogAccessError`.

:::

```cpp
int audio_id = 1; // any integer you choose — your reference to the song

client.custom_catalog().add(
    audio_id, audd::SourceUrl{"https://my.cdn/song.mp3"});

// Or from in-memory bytes:
audd::SourceBytes payload{ /* ... */ };
client.custom_catalog().add(audio_id, payload);
```

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 error raised by the SDK derives from `audd::AudDError`. Server-
reported errors are `audd::AudDApiError` and carry `error_code`,
`message`, `http_status`, `request_id`, `request_method`,
`branded_message`, `requested_params`, and `raw_response`. Network
failures throw `audd::AudDConnectionError`; malformed JSON throws
`audd::AudDSerializationError`; missing token under `strict()` throws
`audd::AudDMissingApiTokenError`; custom-catalog access is denied with
`audd::AudDCustomCatalogAccessError`.

### Idiomatic error handling

Discriminate either with `dynamic_cast`-friendly catch arms or with
`AudDError::category()`. The most common shape is per-type catches for
the failure modes you want to handle and a catch-all for the rest:

```cpp
#include <audd/audd.hpp>
#include <iostream>

try {
    auto song = client.recognize("/path/to/clip.mp3");
    // ... use song
} catch (const audd::AudDMissingApiTokenError& e) {
    std::cerr << "no token: " << e.what() << "\n";
    return 1;
} catch (const audd::AudDCustomCatalogAccessError& e) {
    std::cerr << "custom catalog access denied: " << e.what() << "\n";
} catch (const audd::AudDApiError& e) {
    switch (e.category()) {
    case audd::ErrorCategory::Authentication:
        std::cerr << "check your token: [#" << e.error_code
                  << "] " << e.message << "\n";
        return 1;
    case audd::ErrorCategory::Quota:
    case audd::ErrorCategory::RateLimit:
        std::cerr << "throttled: " << e.message << "\n";
        break;
    case audd::ErrorCategory::InvalidAudio:
        std::cerr << "audio rejected: " << e.message << "\n";
        break;
    default:
        std::cerr << "AudD #" << e.error_code << ": " << e.message
                  << " (request_id=" << e.request_id << ")\n";
    }
} catch (const audd::AudDConnectionError& e) {
    std::cerr << "network: " << e.what() << "\n";
} catch (const audd::AudDSerializationError& e) {
    std::cerr << "bad response body: " << e.what() << "\n";
}
```

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
`AudDApiError::branded_message` rather than surfacing it as a fake
recognition match.

When the server returns code `51` (deprecated parameter) **with** a
usable result, the SDK invokes `ClientConfig::on_deprecation` (default:
write to `std::cerr`) and returns the result as if the call had
succeeded. Code `51` with no result throws `AudDApiError` in the
`InvalidRequest` category.

### Retry behavior

Each endpoint is classified into one of three retry classes:

- **Read** (`streams().list`, `streams().get_callback_url`, longpoll
  HTTP) — retries on any `AudDConnectionError`, plus HTTP 408 / 429 /
  5xx.
- **Recognition** (`recognize`, `recognize_enterprise`,
  `advanced().raw_request`) — retries only on **pre-upload** network
  errors (DNS, connect, TLS) to avoid double-billing for in-progress
  uploads, plus 5xx.
- **Mutating** (`streams().add`, `streams().set_url`, `streams().del`,
  `streams().set_callback_url`, `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 = 500ms` (geometric,
doubles each attempt). Override via `ClientConfig`:

```cpp
audd::ClientConfig cfg;
cfg.max_attempts   = 5;
cfg.backoff_factor = std::chrono::seconds(1);

audd::AudD client("your-api-token", cfg);
```

Set `max_attempts = 1` to disable retries.

## Configuration

Every knob lives on `audd::ClientConfig`, passed to the constructor:

```cpp
audd::ClientConfig cfg;
// ... set fields ...
audd::AudD client("your-api-token", cfg);
```

### Timeouts

Two timeouts live on `ClientConfig`: a short one for the standard
endpoint (and stream / catalog / advanced calls), a long one for
enterprise.

```cpp
audd::ClientConfig cfg;
cfg.standard_timeout   = std::chrono::seconds(30);    // default 90 s
cfg.enterprise_timeout = std::chrono::hours(2);       // default 1 h
```

The 1-hour default for enterprise is intentional: chunked server-side
fingerprinting of multi-hour files takes that long.

### Async and concurrency

A single `audd::AudD` instance is safe to share across threads — the
internal mutex guards token rotation and sub-client lazy construction;
the underlying libcurl handle is per-request.

`AudD` is non-copyable and non-movable (it owns the mutex). Hold it as
an automatic local, in a `std::unique_ptr` (via `AudD::strict(...)`),
or in a `std::shared_ptr`. Pass references where you'd otherwise pass
values.

Every blocking method has an `_async` twin returning `std::future<T>` —
`recognize_async`, `recognize_enterprise_async`, `streams().add_async`,
`streams().list_async`, `custom_catalog().add_async`,
`advanced().raw_request_async`, etc. They run on a default thread pool;
the future is satisfied when the call completes or throws.

### Observability

For per-request observability, register an `on_event` hook:

```cpp
audd::ClientConfig cfg;
cfg.on_event = [](const audd::AudDEvent& e) {
    if (e.kind == audd::AudDEvent::Kind::Response) {
        std::cerr << e.method << " -> " << e.http_status
                  << " in " << e.elapsed.count() << "ms\n";
    }
};

audd::AudD client("your-api-token", cfg);
```

`AudDEvent` is plain data: `kind` (`Request` / `Response` / `Exception`),
`method`, `url`, `request_id`, `http_status`, `elapsed`, `error_code`.
Events never carry the api_token or request body bytes. Hook exceptions
are swallowed so observability cannot break the request path.

### Deprecation hook

Server code 51 ("deprecated parameter") with a usable result triggers
`on_deprecation` instead of throwing:

```cpp
audd::ClientConfig cfg;
cfg.on_deprecation = [](const std::string& msg) {
    my_logger.warn("audd deprecation", msg);
};
```

Default writes to `std::cerr`.

### Token rotation

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

### Retries

Covered under [Retry behavior](#retry-behavior). `max_attempts` and
`backoff_factor` on `ClientConfig`.

### Calling undocumented endpoints

For AudD endpoints not yet wrapped by typed methods on this SDK, hit
them by name through `client.advanced().raw_request(method, params)`. It
returns the parsed JSON body as `nlohmann::json`, runs through the same
retry policy, and surfaces transport / parsing failures through the
usual exception channel. This is the supported path for anything beta
or one-off — typed wrappers ship as features stabilize.

```cpp
auto body = client.advanced().raw_request(
    "getLinks", {{"audd_id", "12345"}});
// body is nlohmann::json — parse with whatever you like.
```

### Build options

CMake cache variables — `AUDD_CXX20` (default `OFF`) toggles C++20;
`AUDD_BUILD_SHARED` / `AUDD_BUILD_STATIC` (both default `ON`) pick the
output library kind; `AUDD_BUILD_EXAMPLES` and `AUDD_BUILD_TESTS`
(default `OFF`) build the runnable examples and the doctest suite;
`AUDD_INSTALL` (default `ON`) generates `install(...)` rules so
consumers can `find_package(audd CONFIG REQUIRED)`. External
requirement: **libcurl** (development headers). nlohmann/json and
cpp-httplib ship vendored.

---

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