# audd-go


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

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

```sh
go get github.com/AudDMusic/audd-go
```

```go
package main

    "fmt"

    audd "github.com/AudDMusic/audd-go"
)

func main() {
    // "test" is the public demo token (10 reqs/day). Get a real one at https://dashboard.audd.io.
    client := audd.NewClient("test")
    result, _ := client.Recognize("https://audd.tech/example.mp3", nil)
    if result != nil {
        fmt.Printf("%s — %s\n", result.Artist, result.Title)
    }
}
```

Every public method has two forms: `Foo(args)` (uses `context.Background()`) and `FooContext(ctx, args)` (takes `ctx` first). The non-context form is the default; use the `*Context` form inside HTTP handlers, pipelines, or any place you already have a `ctx`. The recipes below use the non-context form; see [Configuration](#configuration) for cancellation and deadlines.

## Authentication

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

Token resolution order:

1. The first argument to `NewClient`. Empty string falls through.
2. The `AUDD_API_TOKEN` environment variable.
3. With `NewClientStrict`, construction returns `audd.ErrMissingAPIToken` when neither is set; with `NewClient`, an empty token is accepted and the first API call surfaces a clear server error.

```go
// Explicit.
client := audd.NewClient("your-api-token")

// AUDD_API_TOKEN=your-token  go run main.go
client := audd.NewClient("")

// Fail fast at construction time.
client, err := audd.NewClientStrict("")
if errors.Is(err, audd.ErrMissingAPIToken) {
    log.Fatal("set AUDD_API_TOKEN or pass a token to NewClient")
}
```

The public token `"test"` is fine for examples but caps at 10 requests per day, no streams, no enterprise.

For long-running services that pull tokens from a secret manager and rotate without restart:

```go
if err := client.SetAPIToken(newToken); err != nil {
    log.Fatal(err)
}
current := client.APIToken()
```

`SetAPIToken` swaps the in-effect token under an internal `sync.RWMutex`. In-flight requests that already serialized their multipart body keep using the old token; subsequent requests use the new one.

## Recognize a clip

```go
result, err := client.Recognize("https://audd.tech/example.mp3", nil)
if err != nil {
    log.Fatal(err)
}
if result == nil {
    fmt.Println("no match")
    return
}
fmt.Printf("%s — %s (%s)\n", result.Artist, result.Title, result.Album)
```

`source` is a `Source` (alias for `any`). The SDK accepts:

```go
// URL — AudD downloads server-side.
client.Recognize("https://audd.tech/example.mp3", nil)

// File path — re-opened on each retry attempt.
client.Recognize("/path/to/clip.mp3", nil)

// []byte — always retry-safe.
data, _ := os.ReadFile("clip.mp3")
client.Recognize(data, nil)

// io.Reader — *os.File is also an io.Seeker, so retries work.
file, _ := os.Open("clip.mp3")
defer file.Close()
client.Recognize(file, nil)
```

A non-seekable `io.Reader` is fine on the first attempt; if a retry fires, the SDK returns a clean error rather than sending an empty body.

`Recognize` returns `(*Recognition, error)`. `(nil, nil)` means the request succeeded but no song matched — distinct from an error. A `*Recognition` carries `Artist`, `Title`, `Album`, `ReleaseDate`, `Label`, `Timecode`, `SongLink`, `ISRC`, `UPC`, plus `ThumbnailURL()`, `StreamingURL(provider)`, `StreamingURLs()`, and `PreviewURL()` helpers. See [the full result reference](https://github.com/AudDMusic/audd-go#what-you-get-back).

```go
// Cover art (lis.tn-hosted SongLinks only; "" otherwise).
fmt.Println(result.ThumbnailURL())

// Direct or lis.tn-redirect URL for a specific provider.
fmt.Println(result.StreamingURL(audd.ProviderSpotify))
```

Custom-catalog hits populate `result.AudioID` (rather than `Artist`/`Title`); use `result.IsCustomMatch()` / `result.IsPublicMatch()` to discriminate.

## Process a long audio file

For files longer than ~25 seconds (full-length albums, podcasts, sets, broadcasts) use the enterprise endpoint. It returns a flattened slice of `EnterpriseMatch`.

```go
limit := 1
matches, err := client.RecognizeEnterprise("podcast.mp3", &audd.EnterpriseOptions{
    Limit: &limit,
})
if err != nil {
    log.Fatal(err)
}
for _, m := range matches {
    fmt.Printf("%s  %s — %s\n", m.Timecode, m.Artist, m.Title)
}
```

`EnterpriseOptions` fields are pointer-typed so the zero value means "leave server default":

| Field | Type | Description |
|---|---|---|
| `Limit` | `*int` | Stop after N matches. Enterprise calls bill per 12 seconds of audio processed; set this during development to cap response size. |
| `Skip` | `*int` | Skip the first N chunks. |
| `Every` | `*int` | Process every Nth chunk (sparse mode). |
| `SkipFirstSeconds` | `*int` | Skip the first N seconds of audio. |
| `UseTimecode` | `*bool` | Include `Timecode` in matches. |
| `AccurateOffsets` | `*bool` | Compute precise `StartOffset`/`EndOffset` for each match. Off by default. |

`ISRC`, `UPC`, and `Score` populate only on Startup-tier tokens or higher. Contact [api@audd.io](mailto:api@audd.io) to upgrade.

The enterprise endpoint can take up to an hour for very long files; the client's enterprise timeout defaults to 60 minutes — see [Configuration](#timeouts).

## Get streaming-service metadata

Pass `Return` to ask AudD to fetch metadata from external providers — Apple Music, Spotify, Deezer, Napster, MusicBrainz. Each adds latency.

```go
result, err := client.Recognize(source, &audd.RecognizeOptions{
    Return: []string{"apple_music", "spotify"},
})
if err != nil || result == nil {
    return
}

if result.AppleMusic != nil {
    fmt.Println("Apple Music:", result.AppleMusic.URL)
}
if result.Spotify != nil {
    fmt.Println("Spotify:", result.Spotify.URI)
}
```

Provider sub-objects are non-nil only when requested via `Return`. The full list of providers and the accessor cookbook lives in the [GitHub README](https://github.com/AudDMusic/audd-go#what-you-get-back).

For an undocumented or beta server field that hasn't been promoted to a typed property yet, every model carries an `Extras map[string]json.RawMessage`:

```go
if raw, ok := result.Extras["song_length"]; ok {
    var seconds int
    _ = json.Unmarshal(raw, &seconds)
}
```

## Monitor a live audio stream

Stream recognition turns AudD into a continuous monitor for a live audio stream — internet radio, Twitch, YouTube live, raw HLS / Icecast / DASH — and notifies you every time it identifies a song. Two consumption modes:

- **Callbacks** — set a callback URL once, AudD POSTs JSON to it for each match. Operationally simpler at scale.
- **Longpoll** — pull events from the server. For consumers that can't expose a public URL (CLI tools, mobile apps, browser extensions, scripts behind NAT).

The two next sections cover each. Reach the streams sub-client via `client.Streams()`.

## Receive callbacks from a stream

Three steps: set the account-wide callback URL, subscribe streams to it, parse incoming requests in your handler.

```go
// 1. Register where AudD should POST recognition results.
err := client.Streams().SetCallbackUrl("https://your.app/audd/callback", &audd.SetCallbackUrlOptions{
    ReturnMetadata: []string{"apple_music", "spotify"},
})

// 2. Subscribe streams. RadioID is a slot you assign — use the same RadioID
//    later to update or delete this stream.
radioID := int64(1) // any integer you choose — your handle for this stream
err = client.Streams().Add(audd.AddStreamRequest{
    URL:     "https://example.com/radio.m3u8",
    RadioID: radioID,
})

// Twitch / YouTube shortcuts also work as the URL:
//   "twitch:somechannel", "youtube:<video_id>", "youtube-ch:<channel_id>"
```

Pass `Callbacks: "before"` on `AddStreamRequest` to fire callbacks at song *start* (default is at song end).

In your web app, parse the incoming request with `audd.HandleCallback(*http.Request)`. It returns `(match, notification, error)`: exactly one of `match` / `notification` is non-nil on success. Recognition payloads come as a `*StreamCallbackMatch`; lifecycle and status events come as a `*StreamCallbackNotification`.

```go
package main

    "fmt"
    "log"
    "net/http"

    audd "github.com/AudDMusic/audd-go"
)

func main() {
    http.HandleFunc("/audd/callback", func(w http.ResponseWriter, r *http.Request) {
        match, notif, err := audd.HandleCallback(r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        switch {
        case match != nil:
            fmt.Printf("[radio %d] %s — %s\n",
                match.RadioID, match.Song.Artist, match.Song.Title)
        case notif != nil:
            fmt.Printf("[radio %d] %s\n",
                notif.RadioID, notif.NotificationMessage)
        }
        w.WriteHeader(http.StatusNoContent)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

Other frameworks (gin, chi, echo, gorilla/mux) — see [the GitHub README](https://github.com/AudDMusic/audd-go#streams).

If your callback is consumed by a different process (queue worker, replay tool) and you only have the bytes, use `audd.ParseCallback([]byte)` instead.

To list what AudD knows about for your account, or remove a stream:

```go
streams, _ := client.Streams().List()
for _, s := range streams {
    fmt.Println(s.RadioID, s.URL, "running:", s.StreamRunning)
}

radioID := int64(1) // your stream's handle from when you called Add
err := client.Streams().Delete(radioID)
err = client.Streams().SetURL(radioID, "https://new.example.com/radio.m3u8")
```

## Poll for stream events (longpoll)

For consumers that can't expose a public URL — CLI tools, mobile apps, browser extensions, scripts behind NAT — long-polling pulls events from the server instead of waiting for callbacks.

`LongpollByRadioID` returns a `*LongpollPoll` with three typed channels: `Matches`, `Notifications`, and a single-shot `Errors`. Drive them in a `select` loop and `defer poll.Close()` to tear down the background goroutine.

```go
radioID := int64(1) // your stream's handle from when you called Streams().Add

poll, err := client.Streams().LongpollByRadioID(radioID, nil)
if err != nil {
    log.Fatal(err)
}
defer poll.Close()

for {
    select {
    case m, ok := <-poll.Matches:
        if !ok {
            return
        }
        fmt.Printf("matched: %s — %s\n", m.Song.Artist, m.Song.Title)
    case n, ok := <-poll.Notifications:
        if !ok {
            return
        }
        fmt.Println("notification:", n.NotificationMessage)
    case err := <-poll.Errors:
        log.Fatal(err)
    }
}
```

The server emits a keep-alive every `Timeout` seconds (default 50) when no event fires. The SDK absorbs them silently — your loop only sees real matches and notifications. `Errors` is single-shot: when an error fires, the poll is terminal and the other two channels close.

For cancellation or deadlines, use `LongpollByRadioIDContext(ctx, radioID, opts)` and add a `case <-ctx.Done(): return` arm to the `select`. Cancelling the context exits the background goroutine and closes all three channels.

:::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 call and returns an error 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.

:::

### Tokenless consumers

For browser widgets, mobile apps, or extensions that should never see the api_token, derive a per-stream identifier on your server and ship only that identifier to the client. The client subscribes with it; the api_token stays on your server.

```go
// Server-side (has api_token):
identifier := audd.DeriveLongpollCategory(serverToken, radioID)
// ship `identifier` to the client over your authenticated channel.
```

```go
// Client-side (no api_token, just the identifier from the server):
consumer := audd.NewLongpollConsumer(identifier)
defer consumer.Close()

poll := consumer.Iterate(nil)
defer poll.Close()
// Same select-loop shape as above. Use IterateContext(ctx, nil) for cancellation.
```

The client-side consumer has no api_token to preflight against, so the server is responsible for ensuring a callback URL is configured before sharing identifiers.

## Add a song to your custom catalog

Custom-catalog access requires a separate subscription. Contact [api@audd.io](mailto:api@audd.io) to enable it. Without it, `Add` returns `*AudDCustomCatalogAccessError` (matchable with `errors.Is(err, audd.ErrCustomCatalogAccess)`).

```go
audioID := int64(1) // any integer you choose — your reference to the song
err := client.CustomCatalog().Add(audioID, "/path/to/track.mp3")
if errors.Is(err, audd.ErrCustomCatalogAccess) {
    log.Fatal("custom catalog not enabled on this token")
}
```

`audioID` is a slot identifier you assign. Calling again with the same `audioID` re-fingerprints that slot. The SDK exposes only `Add` — no list / get / delete — so track `audioID` ↔ song mappings on your side. When recognition later hits a custom-catalog song, the resulting `*Recognition` has `AudioID != nil` and `IsCustomMatch()` returns `true`.

## Handle errors

The SDK uses Go's standard error model: typed structs that implement `Error()` and `Is()`, plus sentinel errors that match via `errors.Is`. Use `errors.As` to extract the typed struct for fields like `ErrorCode`, `RequestID`, and `HTTPStatus`.

```go
result, err := client.Recognize(source, nil)
switch {
case errors.Is(err, audd.ErrAuthentication):
    // 900 / 901 / 903 — token problems.
    log.Fatal("check your AUDD_API_TOKEN")
case errors.Is(err, audd.ErrQuota):
    // 902 — quota exceeded.
    log.Println("quota exceeded; back off and retry tomorrow")
case errors.Is(err, audd.ErrRateLimit):
    // 611 — per-stream rate limit.
case errors.Is(err, audd.ErrInvalidAudio):
    // 300 / 400 / 500 — file too small, > 25s/10MB, or not audio.
case errors.Is(err, audd.ErrCustomCatalogAccess):
    // Custom catalog not enabled on this token.
case errors.Is(err, audd.ErrConnection):
    // DNS / TCP / TLS / timeout.
case err != nil:
    var apiErr *audd.AudDAPIError
    if errors.As(err, &apiErr) {
        log.Printf("[#%d] %s (request_id=%s)",
            apiErr.ErrorCode, apiErr.Message, apiErr.RequestID)
    }
}
```

`*AudDAPIError` carries `ErrorCode`, `Message`, `HTTPStatus`, `RequestID`, `RequestedParams`, `RequestMethod`, `BrandedMessage`, and `RawResponse`. `*AudDCustomCatalogAccessError` embeds `*AudDAPIError`. `*AudDConnectionError` wraps the underlying transport error (accessible via `errors.Unwrap`). `*AudDSerializationError` carries the raw text on a 2xx with an unparseable body.

### Retry behavior

The SDK has three retry classes; each method is bound to one at compile time:

| Class | Used by | Retries on net error | Retries on HTTP status |
|---|---|---|---|
| Read | `Streams.List`, `Streams.GetCallbackUrl`, longpoll | Any net error | 408, 429, 5xx |
| Recognition | `Recognize`, `RecognizeEnterprise`, `Advanced.RawRequest` | Pre-upload only (DNS, dial-op `*net.OpError`) | 5xx |
| Mutating | `Streams.Add`/`Delete`/`SetURL`/`SetCallbackUrl`, `CustomCatalog.Add` | Pre-upload only | None |

The cost-protection rule: recognition endpoints don't retry on read-timeout-after-upload because the server may have already done metered work. Mutating endpoints don't retry on 5xx because the side effect may already have happened.

Defaults: 3 attempts, 500 ms initial backoff, exponential with `[0.5x, 1.5x]` jitter, capped at 30 s. Override globally via `WithMaxAttempts(n)` and `WithBackoffFactor(d)`. `WithMaxAttempts(1)` disables retries. Context cancellation aborts retries immediately.

## Configuration

### Context cancellation

Every method has a `*Context` form that takes `ctx context.Context` as its first argument. Cancelling the context aborts in-flight HTTP I/O and exits the retry loop.

```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := client.RecognizeContext(ctx, source, nil)
if errors.Is(err, context.DeadlineExceeded) {
    // The local request was aborted.
}
```

Reach for the `*Context` form inside HTTP handlers, gRPC servers, or anywhere you already have a `ctx` to propagate. The non-context form is just `client.FooContext(context.Background(), args)` under the hood.

:::warning

**Cancellation might still consume requests on the server.** Once the upload has reached AudD, the metered work may already have happened by the time your local cancel signal lands. Use deadlines for SLO enforcement, not for quota control.

:::

### Timeouts

Two HTTP timeouts, set on the underlying `*http.Client`:

| Timeout | Default | Override | Applies to |
|---|---|---|---|
| Standard | 60 s | `WithStandardTimeout(d)` | `Recognize`, Streams, CustomCatalog, longpoll |
| Enterprise | 60 min | `WithEnterpriseTimeout(d)` | `RecognizeEnterprise` only |

The enterprise default is 60 minutes because chunked file processing on the server can take an hour for a 12-hour podcast. For per-call deadlines, use `context.WithTimeout` rather than mutating the client.

### HTTP transport

`WithHTTPClient(*http.Client)` injects a caller-managed transport. Use for corporate proxies, mTLS, response caching, or instrumentation:

```go
hc := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        Proxy:           http.ProxyURL(proxyURL),
        TLSClientConfig: tlsConfig,
    },
}
client := audd.NewClient("your-api-token", audd.WithHTTPClient(hc))
```

The same `*http.Client` is used for both `api.audd.io` and `enterprise.audd.io`. The SDK does not call `CloseIdleConnections` on injected clients during `Close` — caller owns lifecycle. The SDK sets a `User-Agent` of the form `audd-go/<Version> go/<runtime.Version()> (<runtime.GOOS>)` on every request.

### Observability

`WithOnEvent(func(AudDEvent))` registers a hook that fires for every request, response, and exception on the wire. The hook never sees the api_token or the audio body — only structural metadata.

```go
client := audd.NewClient("your-api-token", audd.WithOnEvent(func(e audd.AudDEvent) {
    slog.Info("audd",
        "kind", e.Kind,             // "request" | "response" | "exception"
        "method", e.Method,         // "recognize", "addStream", ...
        "status", e.HTTPStatus,
        "elapsed_ms", e.Elapsed.Milliseconds(),
        "request_id", e.RequestID,
    )
}))
```

The SDK pulls in no metrics or tracing library. Build counters and histograms on top of the hook; propagate `RequestID` as a span attribute.

### Lifecycle

`Client` implements `io.Closer`:

```go
client := audd.NewClient("your-api-token")
defer client.Close()
```

`Close` calls `CloseIdleConnections` on SDK-owned transports and is safe to call multiple times. When a transport was injected via `WithHTTPClient`, `Close` is a no-op for that transport.

### Calling undocumented endpoints

For AudD endpoints not yet wrapped with typed methods, `client.Advanced().RawRequest(method, params)` POSTs `params` as form fields to `https://api.audd.io/<method>/` and returns the parsed JSON body as `map[string]any`. Use only for endpoints not yet exposed through `Recognize`, `Streams.*`, or `CustomCatalog.Add`.

```go
body, err := client.Advanced().RawRequest("newMethodName", map[string]string{
    "param": "value",
})
```

### Migrating from v0.x

`audd-go` is the family exception with prior published versions on the Go module proxy, so it carries thin backwards-compatibility shims. The flat v0 API (`RecognizeByUrl`, `AddStream`, `FindLyrics`, etc.) still works as deprecated wrappers and will be removed in v2.0.0. The current API is namespaced — for example, `client.RecognizeByUrl(url, "apple_music", nil)` becomes `client.Recognize(url, &audd.RecognizeOptions{Return: []string{"apple_music"}})`, and `client.AddStream(url, 7, "before", nil)` becomes `client.Streams().Add(audd.AddStreamRequest{URL: url, RadioID: 7, Callbacks: "before"})`. The `Song` type is a deprecated alias for `Recognition`. Full v0 → v1 mapping in [the GitHub README](https://github.com/AudDMusic/audd-go#migrating-from-v0x).

---

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