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.15Get 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::bytes($buf) — auto-detected. It returns a RecognitionResult on a match, or null when the clip isn't recognized.
For longer audio files (full-length songs, short-form videos, podcasts, broadcasts, 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_metadata 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_metadata: ['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_metadata 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, isrc, upc. It also tells you where the song plays in the file: start_seconds and end_seconds are the match's position in your file, in seconds. These are precise because recognizeEnterprise requests accurate offsets by default (pass accurateOffsets: false to turn that off). Behind them, start_offset and end_offset are the raw fragment-relative offsets in milliseconds. 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 additional fields the SDK doesn't surface as typed properties:
$result = $audd->recognize('https://example.mp3', return_metadata: ['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.
For sending arbitrary form fields the typed parameters don't cover, pass extra_parameters:
$result = $audd->recognize(
'/tmp/snippet.wav',
return_metadata: ['apple_music'],
extra_parameters: ['my_custom_flag' => '1'],
);Typed parameters win on collision.
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.
If the client you inject is a Guzzle client, the SDK passes its timeouts as per-request options, so the timeout: argument and the built-in enterprise defaults still apply. Any other PSR-18 client has no channel for per-request timeouts — configure connect/read timeouts on that client directly, and for long enterprise uploads give it a generous read timeout, up to an hour, when you build it.
Retries. Calls are classified by cost and retried accordingly:
| Class | Endpoints | Retried on |
|---|---|---|
RECOGNITION |
recognize, recognizeEnterprise, advanced->* |
failures to establish the connection, and 5xx responses |
READ |
streams->list, streams->getCallbackUrl, longpoll |
any network error, plus 408, 429, and 5xx |
MUTATING |
streams->setCallbackUrl, streams->add, streams->delete |
failures to establish the connection — never 5xx (the change may already have been applied) |
NONE |
customCatalog->add |
never retried — the upload is metered, and re-sending it could double-bill |
RECOGNITION will not double-bill your account: any failure after the connection is up and the upload is in flight is surfaced rather than retried, because the server may already be processing the audio.
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;
$radioId = 1; // any integer you choose — your handle for this stream
$poll = $audd->streams()->longpoll(radioId: $radioId, 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 longer audio files). 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