Official PHP SDK for music recognition API: identify music from a short audio clip, a long audio file, or a live stream.
The API itself is so simple that it can easily be used even without an SDK: docs.audd.io.
composer require audd/audd:^1.5.7Get your API token at dashboard.audd.io.
Recognize from a URL:
use AudD\AudD;
$audd = new AudD('your-api-token');
$result = $audd->recognize('https://audd.tech/example.mp3');
if ($result !== null) {
echo $result->artist . ' — ' . $result->title;
}Recognize from a local file:
use AudD\AudD;
$audd = new AudD('your-api-token');
$result = $audd->recognize('/path/to/clip.mp3');
if ($result !== null) {
echo $result->artist . ' — ' . $result->title;
}recognize() accepts a URL, a filesystem path, a PSR-7 StreamInterface, a resource handle, or raw bytes wrapped via AudD\Internal\Source::bytes($buf) — auto-detected. It returns a RecognitionResult on a match, or null when the clip isn't recognized.
For files longer than 25 seconds (broadcasts, podcasts, full DJ sets), use recognizeEnterprise($source, limit: ...) — it returns list<EnterpriseMatch>, one per song detected across the file.
Pass the token as a named argument:
$audd = new AudD('your-token');Or omit it and set AUDD_API_TOKEN in the environment — the SDK reads it on construction:
putenv('AUDD_API_TOKEN=your-token');
$audd = AudD::fromEnvironment();AudD::fromEnvironment() is the explicit factory; plain new AudD() does the same env-var lookup but reads less obviously at the call site.
For long-running services that rotate tokens (from a secrets manager, Vault, AWS Parameter Store), call $audd->setApiToken($newToken). Subsequent requests use the new value.
By default recognize() returns the core tags plus AudD's universal song link — no metadata-block opt-in needed:
use AudD\AudD;
use AudD\StreamingProvider;
$audd = new AudD();
$result = $audd->recognize('https://audd.tech/example.mp3');
if ($result === null) {
exit("no match\n");
}
// Core tags
echo $result->artist, ' — ', $result->title, "\n";
echo $result->album, ' / ', $result->release_date, ' / ', $result->label, "\n";
// AudD's universal song page (works in any browser, links into all providers)
echo $result->song_link, "\n";
// Helpers — driven off song_link, work without any return_ opt-in
echo $result->thumbnailUrl(), "\n"; // cover-art URL, or null
echo $result->streamingUrl(StreamingProvider::SPOTIFY), "\n"; // direct or lis.tn redirect
print_r($result->streamingUrls()); // ["spotify" => "...", ...]If you need provider-specific metadata blocks, opt in per call. Request only what you need — each provider you ask for adds latency:
$result = $audd->recognize(
'https://audd.tech/example.mp3',
return_: ['apple_music', 'spotify'],
);
echo $result->apple_music->url, "\n"; // direct Apple Music link
echo $result->spotify->uri, "\n"; // spotify:track:...
echo $result->previewUrl(), "\n"; // first preview across requested providers, or nullValid return_ values: apple_music, spotify, deezer, napster, musicbrainz. The corresponding properties ($result->apple_music, $result->spotify, …) are null when not requested.
EnterpriseMatch (returned by recognizeEnterprise) carries the same core tags plus score, start_offset, end_offset, isrc, upc. Access to isrc, upc, and score requires a Startup plan or higher — contact us for enterprise features.
Every typed model exposes extras carrying any fields the SDK doesn't surface as a typed property. This is the supported way to read undocumented metadata or beta features that aren't yet exposed as typed fields:
$result = $audd->recognize('https://example.mp3', return_: ['apple_music']);
// Top-level extras
$genre = $result->extras['genre'] ?? null;
// Nested extras inside a typed metadata block
$artwork = $result->apple_music->extras['artwork'] ?? null;Magic property access falls through to extras too — $result->genre returns the same value as $result->extras['genre']. Per-account custom fields and beta API responses surface here.
Every server-side error becomes a typed exception. The hierarchy lets you handle whole families with one catch:
AudDException (base)
├── AudDConnectionException network / TLS / timeout
├── AudDSerializationException malformed JSON
├── AudDConfigurationException missing or empty token
└── AudDApiException status=error from server
├── AudDAuthenticationException 900 / 901 / 903
├── AudDQuotaException 902
├── AudDSubscriptionException 904 / 905
│ └── AudDCustomCatalogAccessException 904 from customCatalog
├── AudDInvalidRequestException 50 / 51 / 600 / 601 / 602 / 700–702 / 906
├── AudDInvalidAudioException 300 / 400 / 500
├── AudDStreamLimitException 610
├── AudDRateLimitException 611
├── AudDNotReleasedException 907
├── AudDBlockedException 19 / 31337
├── AudDNeedsUpdateException 20
└── AudDServerException 100 / 1000 / unknown
Idiomatic catch:
use AudD\AudD;
use AudD\Errors\AudDApiException;
use AudD\Errors\AudDAuthenticationException;
use AudD\Errors\AudDInvalidAudioException;
try {
$result = (new AudD())->recognize('https://example.mp3');
} catch (AudDAuthenticationException $e) {
exit("check your token: [#{$e->errorCode}] {$e->apiMessage}\n");
} catch (AudDInvalidAudioException $e) {
echo "audio rejected: {$e->apiMessage}\n";
} catch (AudDApiException $e) {
// catch-all for anything the server reported
echo "AudD #{$e->errorCode}: {$e->apiMessage} (request_id={$e->requestId})\n";
}match works equally well for typed dispatch on the exception class:
catch (AudDApiException $e) {
$action = match (true) {
$e instanceof AudDAuthenticationException => 'reload-token',
$e instanceof AudDRateLimitException => 'back-off',
$e instanceof AudDInvalidAudioException => 'skip',
default => 'log-and-rethrow',
};
}Every AudDApiException carries errorCode, apiMessage, httpStatus, requestId, requestedParams, requestMethod, brandedMessage, and rawResponse — enough to log a full incident or open a support ticket.
The client accepts an optional Psr\Log\LoggerInterface. Pass any PSR-3 implementation — Monolog, Symfony's logger, Laravel's Log channel, or a custom one — and the SDK routes diagnostics through it. The default is NullLogger (silent), and the api_token is never written to a record.
use AudD\AudD;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
$logger = new Logger('audd');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));
$audd = new AudD(apiToken: 'your-token', logger: $logger);Records emitted today: debug for onEvent hook failures (with the exception in context), warning for server-side deprecated-parameter notices (server code 51).
use AudD\AudD;
use GuzzleHttp\Client;
$audd = new AudD(
apiToken: 'your-token',
maxRetries: 3, // per-call retry budget
backoffFactor: 0.5, // initial backoff seconds (jittered)
httpClient: new Client(['proxy' => 'http://corp:8080']),
onEvent: fn ($e) => error_log((string) $e->method),
logger: $logger,
);Custom HTTP client. httpClient accepts any Psr\Http\Client\ClientInterface. Inject a configured Guzzle client, a Symfony PSR-18 adapter, or your own transport to add proxies, mTLS, custom CA bundles, or shared connection pools.
Retries. Calls are classified by cost and retried accordingly:
| Class | Endpoints | Retried on |
|---|---|---|
RECOGNITION |
recognize, recognizeEnterprise, advanced->* |
network errors and 5xx before the upload reaches server |
READ |
streams->list, streams->getCallbackUrl, longpoll |
network errors and 5xx |
MUTATING |
streams->setCallbackUrl, streams->add, streams->delete, customCatalog->add |
network errors and 5xx (idempotent on the server) |
RECOGNITION will not double-bill your account: once the server has accepted bytes, a 5xx after that is surfaced rather than retried.
Inspection. Pass an onEvent closure to receive an AudDEvent for every request / response / exception — useful for metrics, distributed tracing, or attaching requestId to your application logs. Events never carry the api_token or request bytes; exceptions raised from the hook are swallowed and routed through the PSR-3 logger at debug level so observability can't break the request path.
use AudD\AudDEvent;
use AudD\AudDEventKind;
$audd = new AudD(
apiToken: 'your-token',
onEvent: function (AudDEvent $e): void {
if ($e->kind === AudDEventKind::Response) {
error_log("audd {$e->method} -> {$e->httpStatus} ({$e->elapsedMs}ms)");
}
},
);Timeouts. Defaults are 30s connect / 60s read for standard endpoints, and 30s connect / 1 hour read for the enterprise endpoint (which can legitimately process multi-hour files). Override per call with timeout: (seconds).
Real-time recognition off radio streams, broadcast feeds, and any other long-running URL. Configure once, then either receive callbacks on your server or poll for events.
$audd->streams()->setCallbackUrl('https://your.server/audd-callback');
$audd->streams()->add('https://your.stream.url/listen.m3u8', radioId: 42);
foreach ($audd->streams()->list() as $stream) {
echo $stream->radio_id, ' ', $stream->url, ' ', ($stream->stream_running ? 'on' : 'off'), "\n";
}Inside your webhook handler, parse the POST body into a typed result. handleCallback() accepts a PSR-7 ServerRequestInterface, raw JSON bytes, or an already-decoded array — pick whichever your framework gives you:
use AudD\Streams;
// PSR-7 request from your framework:
$result = Streams::handleCallback($request);
// or raw bytes:
$result = Streams::handleCallback(file_get_contents('php://input'));
if ($result->isMatch()) {
$m = $result->match;
echo $m->song->artist, ' — ', $m->song->title, "\n";
foreach ($m->alternatives as $alt) {
echo " alt: ", $alt->artist, ' — ', $alt->title, "\n";
}
} elseif ($result->isNotification()) {
echo 'notification: ', $result->notification->notification_message, "\n";
}Use Streams::parseCallback($array) if you already have the decoded JSON. Both methods are static.
add() accepts direct stream URLs (DASH, Icecast, HLS, m3u/m3u8) and the shortcuts twitch:<channel>, youtube:<video_id>, youtube-ch:<channel_id>.
If you can't expose a public callback URL, longpoll instead. AudD still requires a callback URL to be configured for the account (https://audd.tech/empty/ works as a no-op receiver), and the SDK preflights this for you — pass skipCallbackCheck: true to skip if you've already verified.
use AudD\Models\StreamCallbackMatch;
use AudD\Models\StreamCallbackNotification;
$category = $audd->streams()->deriveLongpollCategory(radioId: 42);
$poll = $audd->streams()->longpoll($category, timeout: 30);
$poll->onMatch(function (StreamCallbackMatch $m): void {
echo $m->song->artist, ' — ', $m->song->title, "\n";
});
$poll->onNotification(function (StreamCallbackNotification $n): void {
echo 'notification: ', $n->notification_message, "\n";
});
$poll->onError(function (\Throwable $e) use ($poll): void {
fwrite(STDERR, $e->getMessage() . "\n");
$poll->close();
});
$poll->run(); // blocks until close() or a terminal errorKeepalive responses ({"timeout":"no events before timeout"}) are silently absorbed — your onMatch/onNotification only fire on real events.
deriveLongpollCategory is a local computation: MD5(MD5(api_token) + radio_id) truncated to 9 hex chars. The category alone is sufficient to subscribe — the api_token is never sent over the wire for longpolls.
For browser widgets, embedded extensions, or any context where shipping the api_token would leak it: derive the category server-side, ship only the category to the consumer, and have the consumer use LongpollConsumer — same callback API, no api_token required:
use AudD\LongpollConsumer;
// $category was derived on your server and shared with this process.
$consumer = new LongpollConsumer(category: 'abc123def');
$poll = $consumer->iterate(timeout: 30);
$poll->onMatch(fn ($m) => print_r($m));
$poll->onError(fn ($e) => fwrite(STDERR, $e->getMessage()));
$poll->run();The custom-catalog endpoint is NOT how you submit audio for music recognition. For recognition, use
recognize()(orrecognizeEnterprise()for files longer than 25 seconds). The custom-catalog endpoint adds songs to your private fingerprint database so futurerecognize()calls on your account can identify your own tracks. Requires special access — contact api@audd.io.
$audd->customCatalog()->add(audioId: 42, source: 'https://my.song.mp3');MIT — see LICENSE.
- Documentation: https://docs.audd.io
- Tokens: https://dashboard.audd.io
- Issues: https://github.com/AudDMusic/audd-php/issues
- Email: api@audd.io