audd-go
Recognize music in audio clips, long broadcasts, and live streams from Go.
go get github.com/AudDMusic/audd-go
package main
import (
"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 for cancellation and deadlines.
Authentication
Get your API token at dashboard.audd.io.
Token resolution order:
- The first argument to
NewClient. Empty string falls through. - The
AUDD_API_TOKENenvironment variable. - With
NewClientStrict, construction returnsaudd.ErrMissingAPITokenwhen neither is set; withNewClient, an empty token is accepted and the first API call surfaces a clear server error.
// 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:
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
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:
// 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.
// 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.
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 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.
Get streaming-service metadata
Pass Return to ask AudD to fetch metadata from external providers — Apple Music, Spotify, Deezer, Napster, MusicBrainz. Each adds latency.
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.
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:
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.
// 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.
package main
import (
"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.
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:
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.
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.
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.
// Server-side (has api_token):
identifier := audd.DeriveLongpollCategory(serverToken, radioID)
// ship `identifier` to the client over your authenticated channel.
// 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 to enable it. Without it, Add returns *AudDCustomCatalogAccessError (matchable with errors.Is(err, audd.ErrCustomCatalogAccess)).
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.
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.
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.
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:
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.
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:
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.
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.