# audd-java


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-java" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://central.sonatype.com/artifact/io.audd/audd" target="_blank" rel="noopener">Maven Central</a>
</div>

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

Maven:

```xml
<dependency>
  <groupId>io.audd</groupId>
  <artifactId>audd</artifactId>
  <version>1.5.7</version>
</dependency>
```

Gradle (Kotlin DSL):

```kotlin
implementation("io.audd:audd:1.5.7")
```

```java

// Get a token at https://dashboard.audd.io — "test" is capped at 10 reqs/day.
try (AudD audd = new AudD("test")) {
    RecognitionResult song = audd.recognize("https://audd.tech/example.mp3");
    System.out.println(song.artist() + " — " + song.title());
}
```

The SDK ships two clients: `AudD` (sync, used above) and `AsyncAudD` (returns `CompletableFuture` from every method that hits the network). Both share the same `AudD.Builder` and configuration knobs — see [Configuration](#configuration).

## Authentication

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

Token resolution applied by both `AudD` and `AsyncAudD`:

1. Explicit constructor / builder argument.
2. `AUDD_API_TOKEN` environment variable.
3. Otherwise: `IllegalArgumentException` at build time, with a message pointing at [dashboard.audd.io](https://dashboard.audd.io).

```java
AudD a = new AudD("your-api-token");                          // explicit
AudD b = AudD.builder().apiToken("your-api-token").build();   // builder
AudD c = AudD.fromEnvironment();                              // env only
```

The public `"test"` token is capped at 10 requests/day for standard recognition only — no enterprise, no streams.

### Token rotation

Long-running services that hot-rotate credentials can swap the token atomically. In-flight requests keep their original token; subsequent ones use the new one.

```java
audd.setApiToken("new-token");
String current = audd.apiToken();
```

## Recognize a clip

`recognize(source)` accepts a URL string, a file path string, a `Path`, a `File`, a `byte[]`, or an `InputStream`:

```java
RecognitionResult song = audd.recognize("https://audd.tech/example.mp3");

RecognitionResult song = audd.recognize(Path.of("clip.mp3"));

byte[] bytes = Files.readAllBytes(Path.of("clip.mp3"));
RecognitionResult song = audd.recognize(bytes);

try (InputStream in = micCaptureStream()) {
    RecognitionResult song = audd.recognize(in);
}
```

`recognize` returns `null` when no music is detected in the clip (a successful response with `result: null`). When the server can't fingerprint the audio at all (file too small, non-audio bytes), it raises `AudDInvalidAudioError` instead — see [Handle errors](#handle-errors).

The `RecognitionResult` exposes getters for `artist()`, `title()`, `album()`, `releaseDate()`, `label()`, `timecode()`, `songLink()`, `isrc()` / `upc()` (enterprise tokens only), the per-provider metadata blocks, plus helpers `thumbnailUrl()` and `streamingUrl(provider)`. See [the GitHub README](https://github.com/AudDMusic/audd-java#what-you-get-back) for the full field list.

```java
RecognitionResult song = audd.recognize(source);
if (song != null) {
    System.out.println(song.artist() + " — " + song.title());
    System.out.println("album: " + song.album());
    System.out.println("released: " + song.releaseDate());
    System.out.println("label: " + song.label());
    System.out.println("timecode: " + song.timecode());
    System.out.println("link: " + song.songLink());
    String thumb = song.thumbnailUrl();      // null for non-lis.tn links
    if (thumb != null) System.out.println("art: " + thumb);
}
```

`extras()` (inherited from `ForwardCompatible`) gives you any field the server returned that the typed model doesn't expose yet — useful for new or undocumented metadata. `rawResponse()` returns the full Jackson `JsonNode`.

## Process a long audio file

The enterprise endpoint accepts files longer than 25 seconds and bills per 12 seconds of audio processed. Use it whenever standard `recognize` would fail with code 400 ("audio over 10MB / 25s"):

```java

List<EnterpriseMatch> matches = audd.recognizeEnterprise(
    Path.of("broadcast.mp3"),
    EnterpriseOptions.builder()
        .limit(10)             // cap response size
        .every(2)              // sample every Nth chunk
        .skipFirstSeconds(60)  // skip the intro
        .build());

for (EnterpriseMatch m : matches) {
    System.out.printf("%s - %s @ %s%n", m.artist(), m.title(), m.timecode());
}
```

:::warning
**Always set `.limit(...)` during development.** Without a limit, a single hour-long file can produce hundreds of metered matches. Set a low cap (5-10) until you've sized your usage.
:::

`EnterpriseOptions` also exposes `skip`, `useTimecode`, `accurateOffsets`, and `timeoutMs`. The async equivalent on `AsyncAudD` returns `CompletableFuture<List<EnterpriseMatch>>`.

## Get streaming-service metadata

Pass `returnMetadata` to populate the per-provider blocks (`apple_music`, `spotify`, `deezer`, `napster`, `musicbrainz`). They are `null` unless requested:

```java

RecognitionResult song = audd.recognize(source,
    RecognizeOptions.builder()
        .returnMetadata("apple_music", "spotify")
        .build());

if (song.appleMusic() != null) {
    System.out.println("apple: " + song.appleMusic().url);
}
if (song.spotify() != null) {
    System.out.println("spotify uri: " + song.spotify().uri);
}
```

For a redirect URL that works regardless of which provider you requested, use `streamingUrl(provider)` — it returns the direct URL when the provider's metadata block is populated, otherwise falls back to the lis.tn `?<provider>` redirect when `songLink` is a lis.tn URL:

```java

String spotify = song.streamingUrl(StreamingProvider.SPOTIFY);
String youtube = song.streamingUrl(StreamingProvider.YOUTUBE);

Map<StreamingProvider, String> all = song.streamingUrls();
```

The `market` option (ISO-3166-1 alpha-2) tweaks Apple Music storefront resolution: `RecognizeOptions.builder().market("US")`. Full provider ID list at [docs.audd.io](https://docs.audd.io/).

## Monitor a live audio stream

An AudD *stream* is a long-running subscription where AudD continuously fingerprints a live audio source (Icecast, HLS, Twitch, YouTube channel, etc.) and notifies you when songs are recognized. Two consumption modes:

- **Callbacks** — AudD POSTs each event to a URL you control. Lowest-latency, requires a public HTTPS endpoint. See [Receive callbacks from a stream](#receive-callbacks-from-a-stream).
- **Longpoll** — your client opens long-running GETs against `/longpoll/` and AudD responds when events fire. Works behind NAT, in browsers, on mobile. See [Poll for stream events (longpoll)](#poll-for-stream-events-longpoll).

Both modes deliver the same `CallbackEvent` shape (a typed sum of match/notification). 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.

Stream subscriptions are paid; contact api@audd.io to enable them on your account.

## Receive callbacks from a stream

Three steps: set the callback URL once, add a stream pointing at it, then parse incoming POST bodies in your web app.

```java

audd.streams().setCallbackUrl("https://example.com/audd-hook");

long radioId = 1L; // any integer you choose — your handle for this stream

audd.streams().add(new AddStreamRequest(
    "https://stream.example.com/radio.mp3",  // direct URL, or twitch:<channel>, youtube:<video_id>, youtube-ch:<channel_id>
    radioId));

// Optional: callbacks at song *start* instead of end
audd.streams().add(new AddStreamRequest(streamUrl, radioId, "before"));
```

`setCallbackUrl` accepts a second argument that appends `?return=<metadata>` to the URL so AudD includes streaming-service metadata in the callback payloads:

```java
audd.streams().setCallbackUrl(
    "https://example.com/audd-hook",
    List.of("apple_music", "spotify"));
```

In your web app, parse incoming POST bodies with `Streams.parseCallback`. It accepts `byte[]`, `InputStream`, or an already-parsed Jackson `JsonNode` and returns a `CallbackEvent` — exactly one of `match()` or `notification()` is present:

```java

CallbackEvent event = Streams.parseCallback(requestBody);
event.match().ifPresent(m -> {
    System.out.printf("%d: %s — %s%n",
        m.radioId(), m.song().artist(), m.song().title());
});
event.notification().ifPresent(n -> {
    System.out.println("notif " + n.notificationCode() + ": " + n.notificationMessage());
});
```

### Spring Boot

```java

@RestController
@RequestMapping("/audd-hook")
public class AudDCallbackController {
    @PostMapping
    public ResponseEntity<Void> handle(@RequestBody byte[] body) {
        CallbackEvent event = Streams.parseCallback(body);
        event.match().ifPresent(m ->
            metrics.record(m.radioId(), m.song().artist(), m.song().title()));
        event.notification().ifPresent(n ->
            log.warn("stream {} notification: {}", n.radioId(), n.notificationMessage()));
        return ResponseEntity.ok().build();
    }
}
```

`@RequestBody byte[]` consumes the raw bytes without intermediate deserialization — `Streams.parseCallback(byte[])` does the JSON decoding. Other frameworks (Servlet API, Javalin, Micronaut) — see [the GitHub README](https://github.com/AudDMusic/audd-java#streams).

### Manage streams

```java

long radioId = 1L; // your stream's handle from when you called add(...)
List<Stream> streams = audd.streams().list();
audd.streams().setUrl(radioId, "https://new-source.example.com/radio.mp3");
audd.streams().delete(radioId);
```

## Poll for stream events (longpoll)

When you can't expose a public HTTPS endpoint to receive callbacks — desktop tools, scripts behind NAT, dev workstations — long-polling delivers the same match and notification events over a blocking GET that the SDK drives in a loop.

Pass the `radio_id` of a stream you've added, register three lambdas, then call `run()`:

```java

int radioId = 1; // the radio_id you used when calling streams.add(...)

try (LongpollPoll poll = audd.streams().longpoll(radioId)) {
    poll.onMatch(m -> System.out.println(m.song().artist() + " — " + m.song().title()))
        .onNotification(n -> System.out.println("notif: " + n.notificationMessage()))
        .onError(err -> log.error("longpoll terminated", err));
    poll.run();   // blocks until close() or terminal error
}
```

`run()` blocks the calling thread until `close()` is invoked or a terminal error fires. The error callback is single-shot — once it runs, the loop exits and no further match/notification callbacks are dispatched. `close()` is idempotent and safe to call from any thread, including from inside a callback. The "no events before timeout" envelope is absorbed transparently; the loop just re-polls.

`LongpollOptions.builder().sinceTime(...).timeout(...).build()` resumes from a server-supplied timestamp and tunes the long-poll wait (default 50 s, max 90 s).

:::warning Longpoll requires a configured callback URL

Even when you only consume events via longpoll, the account must have a callback URL configured — otherwise the longpoll endpoint silently returns keepalives forever. The SDK preflights this on your first `streams.longpoll(...)` call and raises `AudDInvalidRequestError` with guidance if it's missing; pass `LongpollOptions.builder().skipCallbackCheck(true).build()` once you've set one.

If you only want longpoll and have no real receiver, set `https://audd.tech/empty/` — a placeholder URL that discards incoming POSTs:

```java
audd.streams().setCallbackUrl("https://audd.tech/empty/");
```

:::

### Async variant

`AsyncAudD` exposes the same surface, returning a `CompletableFuture<LongpollPoll>` so the preflight check is non-blocking. Drive the returned poll with `runAsync()`, which runs the dispatch loop on a daemon thread:

```java
asyncAudd.streams().longpoll(radioId).thenAccept(poll -> {
    poll.onMatch(m -> ...).onNotification(n -> ...).onError(err -> ...);
    CompletableFuture<Void> done = poll.runAsync();   // completes on close() or error
    // poll.close() from anywhere to stop the loop
});
```

### Tokenless consumers

When the longpoll consumer is an Android app, a desktop client, or a browser widget that should never see your `api_token`, your server hands it an opaque per-stream identifier instead. The client subscribes with that identifier alone:

```java
// Server (has the api_token):
String streamKey = audd.streams().deriveLongpollCategory(radioId);
// ship streamKey to the client over your own auth channel

// Client (no api_token; constructed with any non-empty placeholder):
try (LongpollPoll poll = client.streams().longpoll(streamKey)) {
    poll.onMatch(m -> ...).onNotification(n -> ...).onError(err -> ...);
    poll.run();
}
```

`deriveLongpollCategory` is also exposed as a static `Streams.deriveLongpollCategory(apiToken, radioId)` for callers that want the identifier without instantiating an `AudD` client.

## Add a song to your custom catalog

Custom-catalog access lets AudD recognize *your own* tracks for *your account only*. It's gated — contact api@audd.io if you need it enabled on your token.

```java
long audioId = 1L; // any integer you choose — your reference to the song
audd.customCatalog().add(audioId, Path.of("my-track.mp3"));
```

Recognition results from custom-catalog matches expose the integer `audioId()` you assigned. There is no public list/delete endpoint; track `audioId` ↔ song mappings on your side. Calling `add` with the same ID re-fingerprints that slot.

Without custom-catalog access on your token, `add` throws `AudDCustomCatalogAccessError` (a subclass of `AudDSubscriptionError`).

## Handle errors

### Idiomatic error handling

Every server-side `status=error` raises a typed exception (subclass of `AudDApiError`); all SDK exceptions extend `AudDException` (`RuntimeException`-based, so no `throws` clauses needed). Catch the leaves you care about:

```java

try {
    RecognitionResult song = audd.recognize(clip);
    handle(song);
} catch (AudDAuthenticationError e) {
    log.error("token problem (code {}): {}", e.errorCode(), e.serverMessage());
    rotateToken();
} catch (AudDQuotaError e) {
    log.warn("quota exceeded; backing off");
    backOffUntilNextMonth();
} catch (AudDRateLimitError e) {
    Thread.sleep(5_000);
} catch (AudDInvalidAudioError e) {
    log.warn("not music or too long: code {} — {}", e.errorCode(), e.serverMessage());
} catch (AudDConnectionError e) {
    log.warn("network: {}", e.getMessage(), e.getCause());
}
```

On Java 21+, pattern-match the closed hierarchy:

```java
try {
    audd.recognize(clip);
} catch (AudDException e) {
    switch (e) {
        case AudDQuotaError q          -> backOffUntilNextMonth();
        case AudDRateLimitError r      -> sleepAndRetry();
        case AudDInvalidAudioError ia  -> log.warn("not music: {}", ia.serverMessage());
        case AudDConnectionError ce    -> log.warn("network: {}", ce.getMessage());
        default                        -> throw e;
    }
}
```

`AudDApiError` (and every subclass) exposes `errorCode()`, `serverMessage()`, `httpStatus()`, `requestId()` (quote in support tickets), `requestedParams()` (server's redacted echo, no token), `requestMethod()`, `brandedMessage()`, and `rawResponse()`.

### Retry behavior

The SDK ships three retry classes, applied per-method based on cost-awareness:

| Class | Methods | Retries on HTTP | Retries on exception |
|---|---|---|---|
| `READ` | `streams.list`, `streams.getCallbackUrl`, longpoll loop | 408, 429, 5xx | Any `IOException` (idempotent reads) |
| `RECOGNITION` | `recognize`, `recognizeEnterprise`, `advanced.findLyrics`, `advanced.rawRequest` | 5xx | Pre-upload connection failures only — `ConnectException`, `UnknownHostException`, `SSLHandshakeException`, connect-phase `SocketTimeoutException`. Read-timeout-after-upload is *not* retried (the server may have done metered work). |
| `MUTATING` | `streams.add` / `setUrl` / `delete` / `setCallbackUrl`, `customCatalog.add` | None | Pre-upload connection failures only |

Defaults: `maxRetries = 3` (1 initial + 2 retries), `backoffFactorMs = 500`. Backoff is exponential with full jitter, capped at 30 seconds. `AudDApiError` and subclasses are never retried — they're terminal client-visible failures.

```java
AudD audd = AudD.builder()
    .apiToken("your-api-token")
    .maxRetries(5)
    .backoffFactorMs(1000)
    .build();
```

## Configuration

### Async client

`AsyncAudD` mirrors `AudD` — every method that hits the network returns a `CompletableFuture`. Same configuration knobs, same try-with-resources lifecycle.

```java

try (AsyncAudD audd = AudD.builder().apiToken("your-api-token").buildAsync()) {
    audd.recognize("https://audd.tech/example.mp3")
        .thenAccept(song -> {
            if (song != null) System.out.println(song.artist() + " — " + song.title());
        })
        .join();
}
```

`cancel(true)` on a future interrupts the local `CompletableFuture` chain. If the upload has already reached the server, the request might still be metered — cancel before upload completes to avoid the metered work.

### Timeouts

| Knob | Default | Applies to |
|---|---|---|
| connect timeout | 30 s | All requests (OkHttp default). |
| `standardTimeoutSeconds` | 60 s | `recognize`, `streams.*`, `customCatalog.add`, `advanced.*`. |
| `enterpriseTimeoutSeconds` | 3600 s | `recognizeEnterprise` only. |

```java
AudD audd = AudD.builder()
    .apiToken("your-api-token")
    .standardTimeoutSeconds(120)
    .enterpriseTimeoutSeconds(7200)
    .build();
```

When you inject a custom `OkHttpClient` (below), the SDK does not override its timeouts — your client's settings win.

### Custom HTTP client

Inject a fully-configured `OkHttpClient` for proxies, custom TLS, interceptors, or a shared connection pool:

```java

OkHttpClient http = new OkHttpClient.Builder()
    .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.corp", 8080)))
    .connectTimeout(Duration.ofSeconds(10))
    .readTimeout(Duration.ofSeconds(60))
    .build();

AudD audd = AudD.builder()
    .apiToken("your-api-token")
    .httpClient(http)
    .build();
```

The default User-Agent on every request is `audd-java/<sdk-version> jvm/<java-version> (<os.name>)`.

### Observability

Register a `Consumer<AudDEvent>` to receive lifecycle events for every API call (REQUEST → RESPONSE or EXCEPTION):

```java

AudD audd = AudD.builder()
    .apiToken("your-api-token")
    .onEvent(event -> {
        switch (event.kind()) {
            case REQUEST   -> tracer.startSpan(event.method(), event.url());
            case RESPONSE  -> tracer.endSpan(event.requestId(), event.httpStatus(), event.elapsedMs());
            case EXCEPTION -> tracer.endError(event.method(), event.elapsedMs(), event.extras());
        }
    })
    .build();
```

`AudDEvent` exposes `kind()`, `method()`, `url()`, `requestId()`, `httpStatus()`, `elapsedMs()`, `errorCode()`, and `extras()`. The hook is invoked synchronously; hook exceptions are swallowed at `FINE` log level so observability never breaks the request path. The hook never receives `api_token` or request body bytes.

The SDK also uses `java.util.logging.Logger.getLogger("io.audd")` for code-51 deprecation warnings (when no `onDeprecation` hook is registered) and for hook-exception traces. Bridge to SLF4J / Logback / Log4j 2 via the standard JUL bridge for your stack.

### Concurrency

`AudD` and `AsyncAudD` are both thread-safe — share a single instance across threads. `AsyncAudD` futures complete on OkHttp's dispatcher thread pool by default; chain `.thenApplyAsync(..., executor)` to switch to your own pool. `setApiToken` is atomic (`AtomicReference`-backed), safe to call concurrently with in-flight requests.

### Calling undocumented endpoints

For AudD endpoints not yet wrapped by typed methods, `audd.advanced().rawRequest(method, params)` posts form-encoded params to `/<method>/` and returns the raw Jackson `JsonNode`. The api_token is injected automatically; retries follow the recognition policy.

```java

JsonNode body = audd.advanced().rawRequest("someUndocumentedMethod",
    Map.of("foo", "bar"));
```

`status=error` responses are still parsed into typed exceptions, so error handling matches the rest of the SDK.

---

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