# audd-kotlin


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

Recognize music in audio clips, long broadcasts, and live streams from Kotlin — coroutines-first.

```kotlin
// build.gradle.kts
implementation("io.audd:audd-kotlin:1.5.8")
```

```kotlin

fun main() = runBlocking {
    AudD("your-api-token").use { audd ->   // get yours at dashboard.audd.io
        val song = audd.recognize("https://audd.tech/example.mp3")
        println("${song?.artist} — ${song?.title}")
    }
}
```

The Maven coordinate is `io.audd:audd-kotlin` — distinct from `io.audd:audd`, which is the Java SDK. Every public method on `AudD` is a `suspend fun`; longpoll surfaces matches/notifications/errors as three Kotlin `Flow`s.

## Authentication

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

Token resolution on `AudD(...)` construction:

1. The `apiToken` constructor argument.
2. The `AUDD_API_TOKEN` environment variable.
3. Otherwise the constructor throws `IllegalArgumentException` pointing at [dashboard.audd.io](https://dashboard.audd.io).

```kotlin
val audd = AudD("your-api-token")          // explicit
val audd = AudD(apiToken = null)           // read from AUDD_API_TOKEN
val audd = AudD.fromEnvironment()          // discoverable factory; same as null
```

The public `"test"` token works only against the standard recognition endpoint and is capped at 10 requests/day — fine for hello-worlds, not enough for any real workload.

### Token rotation

Long-running services that pull tokens from a secret manager can rotate without rebuilding the client:

```kotlin
audd.setApiToken("new-token")
```

`setApiToken` is thread-safe — the token cell is an `AtomicReference`. In-flight requests finish on the previous token, subsequent requests pick up the new one. Empty input throws `IllegalArgumentException`. Read the current value with `audd.apiToken`.

## Recognize a clip

```kotlin
val song = audd.recognize("https://audd.tech/example.mp3")
println("${song?.artist} — ${song?.title}")
```

`recognize` returns `null` when the call succeeded but nothing matched. Source forms are accepted via the sealed `Source` class — the SDK auto-detects the variant and produces a per-attempt re-opener so retries don't read from an exhausted handle:

```kotlin

audd.recognize(Source.Url("https://audd.tech/example.mp3"))
audd.recognize(Source.FilePath(File("/clip.mp3")))
audd.recognize(Source.Bytes(File("/clip.mp3").readBytes()))
File("/clip.mp3").inputStream().use { audd.recognize(Source.Stream(it, "clip.mp3")) }
```

The `recognize(url: String, ...)` overload is a convenience for `Source.Url(url)`.

The result projects to a sealed `RecognitionMatch` for exhaustive `when`:

```kotlin
when (val match = song?.toMatch()) {
    is RecognitionMatch.Public -> println(match.value.artist)
    is RecognitionMatch.Custom -> println("custom audio_id=${match.value.audioId}")
    null -> println("no match")
}
```

`recognize` returns a `RecognitionResult?` carrying `artist`, `title`, `album`, `releaseDate`, `label`, `timecode`, `songLink`, helpers like `thumbnailUrl` / `streamingUrl(provider)` / `previewUrl()`, and an `extras` map for unwrapped server fields — see [the full result reference](https://github.com/AudDMusic/audd-kotlin#what-you-get-back).

## Process a long audio file

For files longer than 25 seconds, use `recognizeEnterprise`. It returns a flat list of `EnterpriseMatch` spanning every chunk:

```kotlin
val matches = audd.recognizeEnterprise(
    Source.FilePath(File("/full-show.mp3")),
    limit = 10,
)
for (m in matches) {
    println("${m.timecode}  ${m.artist} — ${m.title}  (score=${m.score})")
}
```

:::warning

Enterprise calls bill per 12 seconds of audio processed. The `limit` argument defaults to `null` (unbounded); pass an explicit value while exploring an unfamiliar input to cap response size.

:::

Other knobs: `skip`, `every`, `skipFirstSeconds`, `useTimecode`, `accurateOffsets`. The enterprise endpoint has 1-hour read/write timeouts (vs. 60 seconds on standard); long broadcasts can take that long to fingerprint server-side.

## Get streaming-service metadata

Pass `returnExtras` to populate provider blocks. Accepts a `String` (`"apple_music,spotify"`), a `List<String>`, or any value with a sensible `toString()`:

```kotlin

val song = audd.recognize(
    "https://audd.tech/example.mp3",
    returnExtras = listOf("apple_music", "spotify"),
) ?: return

// Convenience: resolves direct URL when metadata block was requested,
// falls back to lis.tn redirect.
val appleUrl: String? = song.streamingUrl(StreamingProvider.APPLE_MUSIC)
val previewUrl: String? = song.previewUrl()

// Or read fields off the raw maps (forward-compatible with new server keys).
val artworkUrl: String? = song
    .appleMusic?.get("artwork")
    ?.jsonObject?.get("url")
    ?.jsonPrimitive?.contentOrNull
```

Provider blocks (`appleMusic`, `spotify`, `deezer`, `napster`, `musicbrainz`) are populated **only when requested** via `returnExtras`. Valid wire values: `apple_music`, `spotify`, `deezer`, `napster`, `musicbrainz`, `lyrics`, `timecode`. Each adds latency; ask only for what you'll read. The full provider-ID list is in the [HTTP API reference](/).

## Monitor a live audio stream

A **stream** in AudD is a long-running audio source — typically a radio URL or any HLS/Icecast/SHOUTcast endpoint — that AudD ingests continuously and recognizes against. Each stream has a `radioId` you pick.

Two consumption modes share the same setup:

- **Callbacks** — AudD POSTs each match to your server. Best for production.
- **Longpoll** — your code holds a `LongpollPoll` and reads `Flow`s. Best when you can't expose a public URL (mobile, browser, scripts behind NAT).

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

Three steps:

```kotlin
// 1. Set the callback URL (returnMetadata is baked into the URL as ?return=...).
audd.streams.setCallbackUrl(
    "https://example.com/audd-callback",
    returnMetadata = listOf("apple_music", "deezer"),
)

// 2. Add the stream.
val radioId = 1L // any integer you choose — your handle for this stream
audd.streams.add(
    url = "https://stream.example.com/live.mp3",
    radioId = radioId,
)

// 3. In your web app, parse incoming callback bodies.
when (val event = audd.streams.parseCallback(bodyString)) {
    is CallbackEvent.Match -> {
        val m = event.match
        println("${m.song.artist} — ${m.song.title}")
    }
    is CallbackEvent.Notification -> {
        val n = event.notification
        println("notification ${n.notificationCode}: ${n.notificationMessage}")
    }
}
```

`CallbackEvent` is a sealed interface — exhaustive `when` discriminates between recognition events (`Match`) and stream-lifecycle events (`Notification`, e.g. "stream stopped", "can't connect to the audiostream"). `parseCallback` has three overloads (`JsonObject`, `ByteArray`, `String`); all are pure — no I/O, no `suspend`. The same overloads are exported as free `io.audd.parseCallback(...)` functions.

### Ktor

```kotlin

fun Application.auddCallbacks(audd: AudD) {
    routing {
        post("/audd-callback") {
            val body = call.receiveText()
            when (val event = audd.streams.parseCallback(body)) {
                is CallbackEvent.Match ->
                    log.info("${event.match.song.artist} — ${event.match.song.title}")
                is CallbackEvent.Notification ->
                    log.info("notification: ${event.notification.notificationMessage}")
            }
            call.respond(HttpStatusCode.NoContent)
        }
    }
}
```

Other frameworks (Spring Boot `@RestController`, Javalin, http4k) — see [the GitHub README](https://github.com/AudDMusic/audd-kotlin#streams).

## Poll for stream events (longpoll)

`audd.streams.longpoll(radioId)` opens a subscription and returns a `LongpollPoll` carrying three Kotlin `Flow`s. Collect each in its own coroutine — `coroutineScope { }` keeps them alive together, and `use { }` cancels the background producer when the block exits.

```kotlin

fun main() = runBlocking {
    AudD("your-api-token").use { audd ->
        val radioId = 1L // any integer you choose — your handle for this stream
        audd.streams.longpoll(radioId).use { poll ->
            coroutineScope {
                launch { poll.matches.collect { m ->
                    println("${m.song.artist} — ${m.song.title}")
                } }
                launch { poll.notifications.collect { n ->
                    println("notif ${n.notificationCode}: ${n.notificationMessage}")
                } }
                launch { poll.errors.collect { err -> throw err } }
            }
        }
    }
}
```

`LongpollPoll` exposes:

- `matches: Flow<StreamCallbackMatch>` — recognition events.
- `notifications: Flow<StreamCallbackNotification>` — stream-lifecycle events.
- `errors: Flow<Throwable>` — **single-shot**. The first error fires here and terminates the producer; `matches` and `notifications` then complete.

Server-side timeouts (no match, no notification) are absorbed silently — the loop keeps polling and advances the `since_time` cursor from the server's `timestamp`.

Tune the per-request timeout or resume from a saved cursor with `LongpollOptions`:

```kotlin

audd.streams.longpoll(radioId, LongpollOptions(timeoutSeconds = 30, sinceTime = lastSeen))
    .use { poll -> /* … */ }
```

:::warning Longpoll requires a configured callback URL

If no callback URL is configured for the account, longpoll might never return any events despite successful song recognition. The SDK preflights this on your first `streams.longpoll(...)` call and throws `AudDInvalidRequestException` with guidance if it's missing; pass `LongpollOptions(skipCallbackCheck = true)` once you've set one.

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

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

:::

### Tokenless consumers

When the longpoll consumer is an Android app, desktop GUI, or browser widget that shouldn't carry the api_token, derive an opaque per-stream identifier on a trusted server and ship only that string to the client. The client subscribes to events with the identifier alone — the api_token never leaves the server.

```kotlin
// On the server (has the api_token):
val streamId = audd.streams.deriveLongpollCategory(radioId = 1L)
// → ship `streamId` to the client over your own channel.
```

```kotlin
// On the client (no api_token):

LongpollConsumer(category = streamId).use { consumer ->
    consumer.iterate().use { poll ->
        coroutineScope {
            launch { poll.matches.collect { m -> /* … */ } }
            launch { poll.notifications.collect { n -> /* … */ } }
            launch { poll.errors.collect { err -> throw err } }
        }
    }
}
```

The server side still needs a callback URL configured on the account (`https://audd.tech/empty/` works as a placeholder) — `LongpollConsumer` can't preflight that without a token.

## Add a song to your custom catalog

The custom-catalog endpoint adds a song to your **private fingerprint database** so AudD's recognition can identify your own audio for your account only.

:::warning

Custom-catalog upload requires special access. Contact [api@audd.io](mailto:api@audd.io). Calls without access throw `AudDCustomCatalogAccessException` (a subclass of `AudDSubscriptionException`) with guidance.

:::

```kotlin
val audioId = 1L // any integer you choose — your reference to the song
audd.customCatalog.add(audioId = audioId, source = Source.Url("https://my.cdn/song.mp3"))
```

`audioId` is your identifier; re-uploading with the same `audioId` overwrites that fingerprint slot. There's no public `list` or `delete` — track `audioId` ↔ song mappings on your side. After upload, future `recognize()` calls match against your tracks (the result has `audioId != null` and `result.toMatch()` returns `RecognitionMatch.Custom`).

## Handle errors

### Idiomatic error handling

Every error raised by the SDK is a subclass of `AudDException` (a sealed class extending `RuntimeException`). Catch the specific exceptions you expect and let the rest propagate, or catch `AudDException` to handle the whole family:

```kotlin

try {
    audd.recognize("https://audd.tech/example.mp3")
} catch (e: AudDAuthenticationException) {
    error("check your token: [#${e.errorCode}] ${e.serverMessage}")
} catch (e: AudDQuotaException) {
    error("quota exceeded: [#${e.errorCode}] ${e.serverMessage}")
} catch (e: AudDInvalidAudioException) {
    println("audio rejected: ${e.serverMessage}")
} catch (e: AudDApiException) {
    println("AudD #${e.errorCode}: ${e.serverMessage} (request_id=${e.requestId})")
} catch (e: AudDConnectionException) {
    println("network: ${e.message}")
}
```

Every `AudDApiException` carries `errorCode`, `serverMessage`, `httpStatus`, `requestId`, `requestedParams`, `requestMethod`, `brandedMessage`, and `rawResponse` for incident reproduction. `AudDConnectionException` carries `original: Throwable?` (the underlying Ktor / Java IO exception). `AudDSerializationException` carries `rawText: String`.

For some error codes the AudD server populates `result.artist` / `result.title` with branded text alongside `status: error` (e.g. for `ip_ban`, `request_blocked`, `token_disabled`). Surfacing that as a recognition match would be misleading, so the SDK stashes it on `e.brandedMessage` instead.

### Retry behavior

The SDK classifies every endpoint into one of three retry classes, each tuned to avoid double-billing:

| Class | Endpoints | Retry on exception | Retry on response |
|---|---|---|---|
| `READ` | `streams.list`, `streams.getCallbackUrl`, longpoll polls | any IO / connect / socket-timeout / `UnknownHostException` | HTTP 408, 429, or any 5xx |
| `RECOGNITION` | `recognize`, `recognizeEnterprise` | only pre-upload (`ConnectTimeoutException`, `ConnectException`, `UnknownHostException`) — guards against double-billing for in-progress uploads | any 5xx |
| `MUTATING` | `streams.setCallbackUrl`, `streams.add`, `streams.setUrl`, `streams.delete`, `customCatalog.add` | only pre-upload connection errors | never (the side effect may already have happened) |

Defaults: `maxRetries = 3`, `backoffFactor = 0.5`. Backoff is `min(backoffFactor * 2^attempt, 30)` seconds with `0.5x..1.5x` jitter — so attempts at 0, ~0.5–1.5, ~1.0–3.0 seconds. Override per client:

```kotlin
val audd = AudD("your-api-token", maxRetries = 5, backoffFactor = 1.0)
```

To disable retries entirely, pass `maxRetries = 1`.

## Configuration

### Timeouts

Internal defaults:

| Layer | Connect | Read | Write |
|---|---|---|---|
| Standard | 30 s | 60 s | 60 s |
| Enterprise | 30 s | 3600 s | 3600 s |

Enterprise read/write timeouts are one hour because long files take that long to fingerprint server-side. There is no per-call timeout argument; for finer control, inject a fully-configured `HttpClient`:

```kotlin

val client = HttpClient(CIO) {
    install(HttpTimeout) {
        connectTimeoutMillis = 10_000
        requestTimeoutMillis = 15_000
        socketTimeoutMillis  = 15_000
    }
}
val audd = AudD("your-api-token", httpClient = client)
```

When you inject a single `HttpClient`, both standard and enterprise calls go through it — if your standard-endpoint timeouts conflict with what enterprise needs, leave the SDK to build its own clients.

### Custom HTTP engine

The SDK uses Ktor and accepts either an `HttpClientEngine` (the SDK builds the `HttpClient` around it) or a fully-configured `HttpClient` (the SDK uses it as-is and won't close it):

```kotlin

val audd = AudD("your-api-token", engine = OkHttp.create {
    config {
        retryOnConnectionFailure(false) // SDK manages its own retries
        // proxy, TLS, interceptors, etc.
    }
})
```

The default engine is Ktor `CIO`. Connection pooling, proxies, and TLS are all engine-specific knobs.

### Coroutine context and lifecycle

All public methods are `suspend` — they run on whatever coroutine context the caller provides. Internally, longpoll consumers spawn a `CoroutineScope(SupervisorJob() + Dispatchers.Default)`; that scope is cancelled by `LongpollPoll.close()` (or `use { }`). For one-off calls outside a coroutine, wrap with `runBlocking { }`.

A single `AudD` instance is safe to share across coroutines and threads. `AudD` implements `AutoCloseable` — pair it with `use` for short-lived flows, or call `close()` at shutdown for long-lived processes:

```kotlin
val audd = AudD("your-api-token")
Runtime.getRuntime().addShutdownHook(Thread { audd.close() })
```

When you injected an `HttpClient`, the SDK does **not** close it.

:::warning Server-side metering on cancellation

Coroutine cancellation propagates into the underlying Ktor request, but cancelling a `recognize()` mid-flight might still consume a request on the server — once your bytes have arrived, the metered work may already be in progress.

:::

### Observability

Pass an `onEvent` hook to receive structured per-request events:

```kotlin

val audd = AudD(
    "your-api-token",
    onEvent = { event ->
        println("${event.kind} ${event.method} ${event.httpStatus} ${event.elapsedMs}ms")
    },
)
```

`AudDEvent` is an immutable `data class` with `kind` (`REQUEST` / `RESPONSE` / `EXCEPTION`), `method`, `url`, `requestId`, `httpStatus`, `elapsedMs`, `errorCode`, and an `extras: Map<String, Any?>` for per-event context. Events **never** carry the api_token or response body. Exceptions raised from the hook are swallowed via `runCatching` so observability cannot break the request path.

The SDK also uses the SLF4J logger name `io.audd.AudD`. The default `onDeprecation` hook logs at WARN there; override it to capture or silence:

```kotlin
val audd = AudD(
    "your-api-token",
    onDeprecation = { msg -> myMetrics.increment("audd.deprecation"); println(msg) },
)
```

### Token rotation

Covered in [Authentication](#authentication) — `audd.setApiToken("new-token")` swaps atomically.

### Calling undocumented endpoints

For endpoints not yet wrapped by typed methods, `audd.advanced.rawRequest(method, params)` hits any AudD endpoint by name and returns the raw `JsonObject`:

```kotlin
val body = audd.advanced.rawRequest("getLinks", mapOf("audd_id" to "12345"))
```

Errors decode to the typed exception hierarchy. `RecognitionResult.extras` (and `EnterpriseMatch.extras`, plus the per-provider raw `Map<String, JsonElement>` blocks) carries server fields the SDK doesn't yet type — read them with `kotlinx.serialization.json` accessors. This is the supported path for new server keys until the SDK ships a typed property.

---

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