# audd-swift


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-swift" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://swiftpackageindex.com/AudDMusic/audd-swift" target="_blank" rel="noopener">Swift Package Index</a>
</div>

Recognize music in audio clips, long broadcasts, and live streams from Swift — async/await.

In `Package.swift`:

```swift
.package(url: "https://github.com/AudDMusic/audd-swift", from: "1.5.9"),
```

Then add `.product(name: "AudD", package: "audd-swift")` to your target's dependencies. In Xcode: **File → Add Package Dependencies…** and paste `https://github.com/AudDMusic/audd-swift`.

```swift

@main struct App {
    static func main() async throws {
        let audd = try AudD(apiToken: "test")  // 10 reqs/day; get a real token at dashboard.audd.io
        if let result = try await audd.recognize("https://audd.tech/example.mp3") {
            print("\(result.artist ?? "?") — \(result.title ?? "?")")
        }
    }
}
```

Platforms: macOS 12+, iOS 15+, watchOS 8+, tvOS 15+, visionOS 1+, Linux. Swift 5.9 or newer. No third-party runtime dependencies.

## 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 `apiToken:` argument, if non-`nil` and non-empty.
2. The `AUDD_API_TOKEN` environment variable.
3. Otherwise the initializer throws `AudDError.configuration(...)` with a pointer to [dashboard.audd.io](https://dashboard.audd.io).

```swift
let audd = try AudD(apiToken: "your-api-token")  // explicit
let audd = try AudD()                            // reads AUDD_API_TOKEN
let audd = try AudD.fromEnvironment()            // env-only convenience
```

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

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

```swift
try await audd.setApiToken("new-token")
```

`setApiToken` runs inside the `AudD` actor and is safe to call concurrently. In-flight requests finish on the previous token; subsequent calls use the new one. Passing an empty string throws `AudDError.configuration(...)`.

## Recognize a clip

`recognize(_:)` takes a 5–25-second clip and returns a single `RecognitionResult?` — `nil` when the clip processed but matched nothing.

```swift

let audd = try AudD()

// URL — server fetches the audio
let result = try await audd.recognize("https://audd.tech/example.mp3")

// Filesystem path
let result = try await audd.recognize(.file(URL(fileURLWithPath: "/clip.mp3")))

// Raw bytes
let bytes = try Data(contentsOf: URL(fileURLWithPath: "/clip.mp3"))
let result = try await audd.recognize(.data(bytes))

// InputStream (drained into memory once for retry support)
let stream = InputStream(fileAtPath: "/clip.mp3")!
let result = try await audd.recognize(.stream(stream, name: "clip.mp3"))

if let r = result {
    print("\(r.artist ?? "?") — \(r.title ?? "?") @ \(r.timecode)")
    print("song page:", r.songLink ?? "-")
    print("cover art:", r.thumbnailURL ?? "-")
}
```

A `RecognitionResult` carries `artist`, `title`, `album`, `releaseDate`, `label`, `timecode`, `songLink`, `isrc`/`upc` (enterprise plans), and helpers — `thumbnailURL`, `streamingUrl(_:)`, `streamingUrls()`, `previewUrl()`. Server fields not yet typed surface via `result.extras` (per-model) or `result.rawResponse` (whole payload). Full reference: [github.com/AudDMusic/audd-swift#what-you-get-back](https://github.com/AudDMusic/audd-swift#what-you-get-back).

`Source` notes: `.file` reads fresh on each retry; `.data` reuses the buffer; `.stream` is drained into memory once because `InputStream` isn't portably rewindable on Linux. A `String` overload that doesn't parse as a URL throws `AudDError.invalidArgument(...)`.

## Process a long audio file

For broadcasts, podcasts, and full DJ sets longer than 25 seconds, use the enterprise endpoint. It chunks the file server-side and returns every match.

```swift
let matches = try await audd.recognizeEnterprise(
    .file(URL(fileURLWithPath: "/path/to/broadcast.mp3")),
    limit: 10  // stop after 10 matches; ALWAYS pass this in development
)
for m in matches {
    print("\(m.timecode)  \(m.artist ?? "?") — \(m.title ?? "?")  score=\(m.score)")
}
```

Other accepted args: `skip`, `every`, `skipFirstSeconds`, `useTimecode`, `accurateOffsets`. The endpoint accepts the same `Source` forms as `recognize(_:)`.

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

The SDK defaults `limit:` to `1` to cap unintended billing — pass an explicit `limit:` sized to your use case. The unbounded default on the wire can otherwise ingest many hours of audio in a single call.

:::

Each `EnterpriseMatch` carries `score`, `timecode`, `artist`, `title`, `album`, `releaseDate`, `label`, `songLink`, `isrc`/`upc` (enterprise plans), `startOffset`, `endOffset`, plus `thumbnailURL`, `streamingUrl(_:)`, `streamingUrls()`.

## Get streaming-service metadata

Pass `return:` to populate provider sub-objects on the result. Without it, those fields are `nil`.

```swift
let result = try await audd.recognize(
    "https://audd.tech/example.mp3",
    return: ["apple_music", "spotify"]
)

if let r = result {
    if let am = r.appleMusic {
        print("Apple Music:", am.url ?? "-")
    }
    if let sp = r.spotify {
        print("Spotify URI:", sp.uri ?? "-")
    }

    // Or resolve any provider — direct URL when the metadata block is set,
    // else the lis.tn redirect when songLink is on lis.tn.
    print(r.streamingUrl(.spotify) ?? "-")
    print(r.streamingUrls())  // every provider with a resolvable URL
    print(r.previewUrl() ?? "-")  // 30-second preview, provider terms apply
}
```

Valid providers: `apple_music`, `spotify`, `deezer`, `napster`, `musicbrainz`. Each provider you ask for adds latency.

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

```swift
let result = try await audd.recognize(
    "https://audd.tech/example.mp3",
    return: ["apple_music"],
    market: "GB"
)
```

For the field shape of each provider sub-object, see the upstream API reference: [docs.audd.io](/) and the [GitHub README](https://github.com/AudDMusic/audd-swift#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 `radioID` you choose.

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

```swift
try await audd.streams.setCallbackURL(
    "https://your-app.example.com/audd-callback",
    returnMetadata: ["apple_music", "spotify"]  // optional; populates each callback
)
```

`returnMetadata` is serialized into the URL as `?return=apple_music,spotify` (the server reads it from the URL). If the URL already contains `return=` and `returnMetadata` is also non-`nil`, the SDK throws rather than silently overwriting.

### 2. Register the stream

```swift
let radioID: Int64 = 1  // any integer you choose — your handle for this stream

try await audd.streams.add(
    url: "https://radio.example.com/stream.mp3",
    radioID: radioID
)
```

Other stream methods:

```swift
try await audd.streams.setURL(radioID: radioID, url: "https://radio.example.com/new.mp3")
try await audd.streams.delete(radioID: radioID)
let streams = try await audd.streams.list()  // [Stream] with radioID, url, streamRunning, longpollCategory
```

### 3. Parse incoming POST bodies

`streams.parseCallback(_:)` is a pure function — no I/O — that takes the raw body bytes and returns a `CallbackEvent`:

```swift
public enum CallbackEvent: Sendable, Equatable {
    case match(StreamCallbackMatch)
    case notification(StreamCallbackNotification)
}
```

Recognition events arrive as `.match`; stream-lifecycle events (`"stream stopped"`, `"can't connect"`, etc.) as `.notification`. The same parser is used by the longpoll loop internally, so the parsed shapes are identical regardless of consumption mode.

#### Vapor — canonical recipe

[Vapor](https://vapor.codes) is the dominant async/await Swift web framework. Mount the AudD callback at any route and pass the request body through `parseCallback`:

```swift

let audd = try AudD()

func routes(_ app: Application) throws {
    app.post("audd-callback") { req async throws -> HTTPStatus in
        let bodyBuffer = req.body.data ?? ByteBuffer()
        let data = Data(buffer: bodyBuffer)

        let event = try await audd.streams.parseCallback(data)
        switch event {
        case .match(let m):
            req.logger.info(
                "audd match",
                metadata: [
                    "radio_id": "\(m.radioID)",
                    "artist": "\(m.song.artist)",
                    "title": "\(m.song.title)",
                ]
            )
            // persist, queue, fan-out, etc.

        case .notification(let n):
            req.logger.info(
                "audd notification",
                metadata: [
                    "radio_id": "\(n.radioID)",
                    "code": "\(n.notificationCode)",
                    "message": "\(n.notificationMessage)",
                ]
            )
        }

        return .ok
    }
}
```

A `StreamCallbackMatch` carries `radioID`, `timestamp`, `playLength`, `song` (the top match), `alternatives` (rare extra candidates — **may have different artist/title** than the top match), and `rawResponse` (the full unparsed body). A `StreamCallbackSong` mirrors a `RecognitionResult`'s metadata: `artist`, `title`, `score`, `album`, `releaseDate`, `label`, `songLink`, plus optional provider blocks when `returnMetadata` was set. A `StreamCallbackNotification` carries `radioID`, `streamRunning`, `notificationCode`, `notificationMessage`, and `time`.

Other frameworks (NIO HTTP1, Hummingbird) — see [the GitHub README](https://github.com/AudDMusic/audd-swift#streams).

## Poll for stream events (longpoll)

When your process can't expose a public callback URL — iOS apps, Mac tools, scripts behind NAT, anything that can't accept inbound HTTP — call `streams.longpoll(radioID:)` and consume matches as an `AsyncSequence`.

:::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 `AudDError.api(...)` with `kind == .invalidRequest` and 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:

```swift
try await audd.streams.setCallbackURL("https://audd.tech/empty/")
```

:::

```swift
let radioID = 1  // any integer you choose — your handle for this stream
let poll = try await audd.streams.longpoll(radioID: radioID)
defer { Task { await poll.close() } }

for await match in poll.matches {
    print("\(match.song.artist) — \(match.song.title)")
}
```

`longpoll(radioID:)` returns a `LongpollPoll` actor with three typed `AsyncStream`s — `matches`, `notifications`, `errors` — fed by a background task. `errors` is single-shot: the first terminal error fires there and then all three streams close.

To consume matches, lifecycle notifications, and the terminal error concurrently, use a task group:

```swift

let audd = try AudD()
let poll = try await audd.streams.longpoll(
    radioID: 1,
    options: LongpollOptions(timeout: 30)
)

await withTaskGroup(of: Void.self) { group in
    group.addTask {
        for await m in poll.matches {
            print("match: \(m.song.artist) — \(m.song.title)")
        }
    }
    group.addTask {
        for await n in poll.notifications {
            print("notif #\(n.notificationCode): \(n.notificationMessage)")
        }
    }
    group.addTask {
        for await err in poll.errors {
            print("terminal: \(err)")
            await poll.close()
            return
        }
    }
}
```

`close()` is idempotent. Calling it cancels the background task and finishes all three streams so any in-flight `for await` exits cleanly. `Task.cancel()` on the enclosing task does the same.

### Tokenless consumers

When the consumer is a client process that should not hold the api_token — iOS app talking to your Vapor backend, Mac frontend, browser-style widget — your server derives an opaque per-stream identifier, ships only that string to the consumer, and the consumer subscribes with `LongpollConsumer`. The api_token never leaves the server.

On the server (which holds the api_token):

```swift
let identifier = await audd.streams.deriveLongpollCategory(radioID: 1)
// ship `identifier` to the consumer over your own transport
```

In the consumer (no api_token; just the identifier):

```swift

let consumer = LongpollConsumer(category: receivedIdentifier)
defer { consumer.close() }

let poll = consumer.iterate()
defer { Task { await poll.close() } }

for await match in poll.matches {
    print("\(match.song.artist) — \(match.song.title)")
}
```

The server is responsible for ensuring a callback URL is configured on the account; the consumer can't preflight that without a token.

## 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 `recognizeEnterprise(_:)` for files longer than 25 seconds).

:::warning

The custom-catalog endpoint requires special access. Contact [api@audd.io](mailto:api@audd.io) to enable it. Calls without access throw `AudDError.api(...)` with `kind == .customCatalogAccess` and a contact-us message.

:::

```swift
let audioID: Int64 = 1  // any integer you choose — your reference to the song
try await audd.customCatalog.add(
    audioID: audioID,
    source: .url(URL(string: "https://my.cdn/song.mp3")!)
)
```

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

## Handle errors

Every error thrown by the SDK is a case of `AudDError`. The enum is `Sendable` and conforms to `LocalizedError` (so `error.localizedDescription` produces a human-readable summary). Pattern-match on cases — and on `detail.kind` for API errors — to handle whole families.

```swift
public enum AudDError: Error, Sendable {
    case api(AudDAPIErrorDetail)
    case serverError(httpStatus: Int, message: String, requestID: String?, rawText: String)
    case connection(message: String, underlying: Error?)
    case serializationError(message: String, rawText: String)
    case invalidArgument(String)
    case unsupportedSource(String)
    case configuration(String)
}
```

`AudDAPIErrorDetail` carries `kind` (the classified family — see the catalog below), `errorCode`, `message`, `httpStatus`, `requestID`, `requestedParams`, `requestMethod`, `brandedMessage`, `rawResponse`.

### Idiomatic catch

```swift

do {
    _ = try await audd.recognize("https://audd.tech/example.mp3")
} catch let AudDError.api(detail) where detail.kind == .authentication {
    // 900 / 901 / 903
    print("check your token: [#\(detail.errorCode)] \(detail.message)")
} catch let AudDError.api(detail) where detail.kind == .quota {
    // 902
    print("out of quota: \(detail.message)")
} catch let AudDError.api(detail) where detail.kind == .rateLimit {
    // 611, HTTP 429
    print("rate limited: \(detail.message)")
} catch let AudDError.api(detail) where detail.kind == .invalidAudio {
    // 300 / 400 / 500
    print("audio rejected: \(detail.message)")
} catch let AudDError.api(detail) where detail.kind == .customCatalogAccess {
    // 904 from custom_catalog.add
    print("custom catalog: \(detail.message)")
} catch let AudDError.api(detail) {
    // Catch-all for any server-reported error
    print("AudD #\(detail.errorCode): \(detail.message) (request_id=\(detail.requestID ?? "-"))")
} catch AudDError.connection(let message, _) {
    print("connection: \(message)")
} catch AudDError.serverError(let status, let message, _, _) {
    print("server error HTTP \(status): \(message)")
} catch AudDError.serializationError(let message, _) {
    print("decode: \(message)")
}
```

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

```swift
do {
    _ = try await audd.recognize(...)
} catch let AudDError.api(detail) where detail.kind == .blocked {
    print("[#\(detail.errorCode)] \(detail.message)")
    if let branded = detail.brandedMessage {
        print("server brand text: \(branded)")
    }
}
```

When the server returns code `51` (deprecated parameter) **with** a usable result, the SDK logs a deprecation message via `AudDLog.deprecation` and returns the result as if the call had succeeded. Code `51` with no result throws `AudDError.api(...)` with `kind == .invalidRequest`.

### Retry behavior

Each endpoint is classified into one of three retry classes:

- **`.read`** (`streams.list`, `streams.getCallbackURL`, longpolls) — retries on any `URLError`, plus HTTP 408/429/5xx.
- **`.recognition`** (`recognize`, `recognizeEnterprise`, `advanced.*`) — retries only on **pre-upload** `URLError`s (`.cannotConnectToHost`, `.cannotFindHost`, `.dnsLookupFailed`, `.timedOut`, `.secureConnectionFailed`, `.networkConnectionLost`, `.notConnectedToInternet`) to avoid double-billing for in-progress uploads, plus 5xx.
- **`.mutating`** (`streams.add`, `streams.setURL`, `streams.delete`, `streams.setCallbackURL`, `customCatalog.add`) — retries only on pre-upload `URLError`s; 5xx is surfaced because the side effect may have already happened.

Defaults: `maxRetries = 3`, `backoffFactor = 0.5`. Backoff is `min(backoffFactor * 2.0^attempt, 30.0)` with `0.5x..1.5x` jitter. Override per client:

```swift
let audd = try AudD(apiToken: "your-token", maxRetries: 5, backoffFactor: 1.0)
let audd = try AudD(maxRetries: 1)  // disable retries
```

## Configuration

### Timeouts

The SDK builds two HTTP clients with different timeout profiles:

| Layer | Connect (`timeoutIntervalForRequest`) | Overall (`timeoutIntervalForResource`) |
|---|---|---|
| Standard | 30 s | 60 s |
| Enterprise | 30 s | 3600 s |
| Tokenless longpoll (`LongpollConsumer`) | 10 s | 120 s |

The enterprise resource timeout is one hour because long files take that long to fingerprint server-side. For finer-grained control, inject a custom `URLSession` (see below) with the `URLSessionConfiguration` you need. Use `enterpriseURLSession:` to assign a longer-timeout session to enterprise calls only.

### Custom URLSession

Inject your own `URLSession` for proxies, mTLS, certificate pinning, or to share an existing pool with the rest of your app:

```swift

let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
    "HTTPSEnable": true,
    "HTTPSProxy": "corp-proxy",
    "HTTPSPort": 8080,
]
let session = URLSession(configuration: config)

let audd = try AudD(apiToken: "your-api-token", urlSession: session)
```

The SDK adds its `User-Agent` header on every request whether the session is injected or not. The injected session is **not** invalidated when you call `audd.close()` — the SDK only invalidates sessions it built itself.

### Observability

The SDK emits a structured event on every request lifecycle phase via the `onEvent:` constructor hook:

```swift

let audd = try AudD(
    apiToken: "your-token",
    onEvent: { event in
        switch event.kind {
        case .request:
            break  // about to send
        case .response:
            print("\(event.method) \(event.httpStatus ?? 0) request_id=\(event.requestId ?? "-") (\(event.elapsed ?? 0)s)")
        case .exception:
            print("\(event.method) failed: \(event.extras["error_type"]?.value ?? "unknown")")
        }
    }
)
```

`AudDEvent` is a `Sendable` struct with `kind` (`.request` / `.response` / `.exception`), `method`, `url`, `requestId`, `httpStatus`, `elapsed`, `errorCode`, and a free-form `extras` dict. Events never carry the api_token or request body bytes. Hook exceptions are swallowed so observability cannot break the request path.

### Token rotation

Covered under [Authentication](#authentication) — `try await audd.setApiToken("new-token")` swaps atomically without aborting in-flight requests.

### Retries

Covered under [Retry behavior](#retry-behavior). `maxRetries:` and `backoffFactor:` on the constructor.

### Concurrency and lifecycle

`AudD` is a Swift `actor` — every public method is `async` and serializes through actor isolation. Share one instance across tasks and structured-concurrency contexts; you don't need any explicit synchronization. `Streams`, `CustomCatalog`, `Advanced`, and every model type are `Sendable`. `LongpollPoll` is itself an `actor`.

Cancellation propagates cleanly: `Task.cancel()` aborts the underlying `URLSessionDataTask` and bubbles `CancellationError` (or `URLError(.cancelled)`) back to the caller. The longpoll loop checks `Task.isCancelled` between iterations and exits.

`close()` invalidates the SDK-owned `URLSession`s eagerly via `finishTasksAndInvalidate()`. `deinit` also calls `close()`, so explicit close is only useful for determinism (CLI tools, tests, scoped resource ownership).

```swift
let audd = try AudD()
defer { Task { await audd.close() } }
```

:::warning Cancellation does not refund server-side metering

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.rawRequest(method:params:)`. It returns the raw JSON body as a `[String: Any]`, runs through the same retry policy as recognition, and throws `AudDError.serializationError(...)` if the response isn't a JSON object. This is the supported path for anything beta or one-off — typed wrappers ship as features stabilize.

```swift
let body = try await audd.advanced.rawRequest(
    method: "getLinks",
    params: ["audd_id": "12345"]
)
```

---

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