Skip to main content

audd-php

Open .md

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

composer require audd/audd
<?php

require __DIR__ . '/vendor/autoload.php';

use AudD\AudD;

$audd = new AudD('test'); // 10 reqs/day; get a real token at dashboard.audd.io
$song = $audd->recognize('https://audd.tech/example.mp3');
if ($song !== null) {
echo $song->artist, ' — ', $song->title, "\n";
}

PHP 8.1+. Synchronous — one client per request scope.

Authentication​

Get your API token at dashboard.audd.io.

The api_token is resolved on construction in this order:

  1. The first positional argument: new AudD('your-api-token').
  2. The AUDD_API_TOKEN environment variable.
  3. Otherwise AudDConfigurationException, with a pointer to dashboard.audd.io.
use AudD\AudD;

$audd = new AudD('your-api-token'); // explicit
$audd = new AudD(); // reads AUDD_API_TOKEN
$audd = AudD::fromEnvironment(); // same as above, clearer at the call site

The public "test" token is capped at 10 requests/day and works only for standard recognition — fine for hello-worlds, not for production.

For long-running CLI workers or queue consumers pulling tokens from a secret manager, rotate without rebuilding the client:

$audd->setApiToken('new-token');

PHP is request-scoped, so there are no in-flight calls to coordinate with — the next call uses the new token. Passing an empty string raises AudDConfigurationException.

Recognize a clip​

recognize() takes a short audio clip / 10 MB and returns a single RecognitionResult on a match, or null when the clip processed but matched nothing.

use AudD\AudD;
use AudD\Internal\Source;

$audd = new AudD();

// URL — server fetches the audio
$result = $audd->recognize('https://audd.tech/example.mp3');

// Filesystem path
$result = $audd->recognize('/path/to/clip.mp3');

// PSR-7 stream from a framework request, an S3 SDK, or a stream factory
$result = $audd->recognize($psrStream);

// Raw bytes — explicit wrapper avoids ambiguity with file paths
$result = $audd->recognize(Source::bytes(file_get_contents('/path/to/clip.mp3')));

if ($result !== null) {
echo "{$result->artist} — {$result->title} @ {$result->timecode}\n";
echo 'song page: ', $result->song_link, "\n";
echo 'cover art: ', $result->thumbnailUrl(), "\n";
}

A RecognitionResult carries artist, title, album, release_date, label, timecode, song_link, isrc / upc (enterprise plans), and helpers — thumbnailUrl(), streamingUrl($provider), streamingUrls(), previewUrl(), isCustomMatch(), isPublicMatch(). Server fields not yet typed surface via $result->extras (or magic __get: $result->genre). Full reference: github.com/AudDMusic/audd-php#what-you-get-back.

Source-form notes: filesystem paths reopen on each retry, raw bytes copy on each retry, PSR-7 streams record tell() and seek() back — so unseekable streams cannot be retried (buffer to bytes via Source::bytes(...) first if you need that). A string that's neither an http(s):// URL nor an existing file raises InvalidArgumentException.

Process a long audio file​

For full-length songs, short-form videos, podcasts, broadcasts, and DJ sets, use the enterprise endpoint. It chunks the file server-side and returns every match.

$matches = $audd->recognizeEnterprise(
'/path/to/broadcast.mp3',
limit: 10, // stop after 10 matches; ALWAYS set this in development
);
foreach ($matches as $m) {
printf("%s %s — %s (score=%d)\n", $m->timecode, $m->artist, $m->title, $m->score);
}

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

Enterprise calls bill per 12 seconds of audio processed

Always set limit: during development. The unbounded default can ingest many hours of audio on a single call.

Each EnterpriseMatch carries score, timecode, artist, title, album, release_date, label, song_link, isrc/upc (enterprise plans), start_offset, end_offset, plus thumbnailUrl(), streamingUrl($provider), streamingUrls().

Get streaming-service metadata​

Pass return_metadata: to populate provider sub-objects on the result. Without it, those fields are null.

$result = $audd->recognize(
'https://audd.tech/example.mp3',
return_metadata: ['apple_music', 'spotify'],
);

if ($result !== null) {
if ($result->apple_music !== null) {
echo 'Apple Music: ', $result->apple_music->url, "\n";
}
if ($result->spotify !== null) {
echo 'Spotify URI: ', $result->spotify->uri, "\n";
}

// Or resolve any provider — direct URL when the metadata block is set,
// else the lis.tn redirect when song_link is on lis.tn.
use AudD\StreamingProvider;
echo $result->streamingUrl(StreamingProvider::SPOTIFY), "\n";
print_r($result->streamingUrls()); // all providers with a resolvable URL
echo $result->previewUrl(), "\n"; // 30-s preview, provider terms apply
}

Valid providers: apple_music, spotify, deezer, musicbrainz. Each provider adds latency.

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

$result = $audd->recognize($url, return_metadata: ['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.

Pass arbitrary form fields​

For form fields the SDK doesn't expose as typed parameters — additional options, beta features, account-specific flags — pass extra_parameters:

$result = $audd->recognize(
'/tmp/snippet.wav',
return_metadata: ['apple_music'],
extra_parameters: ['my_custom_flag' => '1'],
);

Typed parameters win on collision. Available on recognize, recognizeEnterprise, streams.setCallbackUrl, and streams.add.

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 radio_id you pick.

There are two ways to consume recognition events:

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​

$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 you've already baked ?return= into the URL string and also pass returnMetadata, the SDK raises rather than silently overwriting.

2. Register the stream​

$radioId = 1; // any integer you choose — your handle for this stream

$audd->streams()->add(
url: 'https://radio.example.com/stream.mp3',
radioId: $radioId,
);

add() accepts direct stream URLs (DASH, Icecast, HLS, m3u/m3u8) and three shortcuts: twitch:<channel>, youtube:<video_id>, youtube-ch:<channel_id>. Pass callbacks: 'before' to deliver callbacks at song start instead of song end.

Other stream methods:

$audd->streams()->setUrl(radioId: $radioId, url: 'https://radio.example.com/new.mp3');
$audd->streams()->delete(radioId: $radioId);
$streams = $audd->streams()->list(); // list<Stream> with radio_id, url, stream_running, longpoll_category

3. Parse incoming POST bodies — Laravel controller​

Each callback POST body is JSON with either a result block (recognition) or a notification block (lifecycle event). Streams::handleCallback($input) accepts a PSR-7 ServerRequestInterface, a PSR-7 StreamInterface, raw JSON string, or a pre-decoded array. Streams::parseCallback(array $body) is a pure function for already-decoded bodies — useful for queue replay or webhook proxies.

Both are static, so the route handler doesn't need an AudD instance to parse — only to manage the streams that produce the events.

<?php

namespace App\Http\Controllers;

use AudD\Streams;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

final class AudDCallbackController
{
public function __invoke(Request $request): Response
{
$event = Streams::handleCallback($request->getContent());

if ($event->isMatch()) {
$m = $event->match;
logger()->info("audd: {$m->song->artist} — {$m->song->title}", [
'radio_id' => $m->radio_id,
'score' => $m->song->score,
]);
// persist, queue, fan-out, etc.
} elseif ($event->isNotification()) {
// Stream lifecycle: "stream stopped", "can't connect", etc.
$n = $event->notification;
logger()->info("audd: {$n->notification_message}", [
'radio_id' => $n->radio_id,
'code' => $n->notification_code,
]);
}

return response()->noContent();
}
}

CallbackEvent carries exactly one of match (a StreamCallbackMatch) or notification (a StreamCallbackNotification); isMatch() / isNotification() discriminate. A match exposes radio_id, timestamp, play_length, song (the top match), alternatives (rare extra candidates — may have different artist/title from the top match), and rawResponse (the full unparsed body). A StreamCallbackSong mirrors a RecognitionResult's metadata: artist, title, score, album, release_date, label, song_link, plus optional provider blocks when returnMetadata was set. A notification carries radio_id, stream_running, notification_code, notification_message, and time.

Other patterns (bare PHP via php://input, Symfony controllers) — see the GitHub README.

Poll for stream events (longpoll)​

For consumers that can't expose a public callback URL — CLI workers, scripts behind NAT, queue workers, Laravel artisan commands — poll /longpoll/ instead. Your process receives matches and notifications synchronously over HTTP.

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 raises AudDInvalidRequestException with guidance if it's missing; pass 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:

$audd->streams()->setCallbackUrl('https://audd.tech/empty/');

Call longpoll(radioId: ...) to get a LongpollPoll handle. Register onMatch, onNotification, and onError closures, then call run() — it blocks until close() is called or a terminal error fires.

use AudD\Models\StreamCallbackMatch;
use AudD\Models\StreamCallbackNotification;

$poll = $audd->streams()->longpoll(radioId: 1, 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 error

Pass the same radioId you used when adding the stream. Keepalive responses from the server are silently absorbed; the loop advances internally and continues. To stop from another thread or signal handler, call $poll->close() — it's idempotent and safe from inside any callback.

Tokenless consumers​

For browser widgets, desktop apps, or any client that should not see the api_token: have your server derive an opaque per-stream identifier and ship just that string to the client. The client subscribes via LongpollConsumer and never sees the token.

On the server (with the api_token):

$category = $audd->streams()->deriveLongpollCategory(radioId: 1);
// ship $category to the client over your own channel

On the client (no api_token needed):

use AudD\LongpollConsumer;

$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 server side (which holds the token) is responsible for ensuring a callback URL is configured — LongpollConsumer can't preflight that.

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()).

danger

The custom-catalog endpoint requires special access. Contact api@audd.io to enable it. Calls without access raise AudDCustomCatalogAccessException.

$audioId = 1; // any integer you choose — your reference to the song
$audd->customCatalog()->add(audioId: $audioId, source: '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 raised by the SDK subclasses AudD\Errors\AudDException. Server-reported errors subclass AudDApiException and carry errorCode, apiMessage, httpStatus, requestId, requestedParams, requestMethod, brandedMessage, and rawResponse. Network failures raise AudDConnectionException; malformed JSON raises AudDSerializationException.

Idiomatic error handling​

use AudD\AudD;
use AudD\Errors\AudDApiException;
use AudD\Errors\AudDAuthenticationException;
use AudD\Errors\AudDConnectionException;
use AudD\Errors\AudDInvalidAudioException;
use AudD\Errors\AudDQuotaException;
use AudD\Errors\AudDRateLimitException;

try {
$result = (new AudD())->recognize('/path/to/clip.mp3');
} catch (AudDAuthenticationException $e) {
exit("check your token: [#{$e->errorCode}] {$e->apiMessage}\n");
} catch (AudDQuotaException $e) {
error_log("out of quota: {$e->apiMessage}");
} catch (AudDRateLimitException $e) {
error_log("rate limited: {$e->apiMessage}");
} catch (AudDInvalidAudioException $e) {
error_log("audio rejected: {$e->apiMessage}");
} catch (AudDApiException $e) {
error_log("AudD #{$e->errorCode}: {$e->apiMessage} (request_id={$e->requestId})");
} catch (AudDConnectionException $e) {
error_log("network: {$e->getMessage()}");
}

match (true) works 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',
};
}

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 the exception's brandedMessage rather than surfacing it as a fake recognition match:

use AudD\Errors\AudDBlockedException;

try {
$audd->recognize('https://audd.tech/example.mp3');
} catch (AudDBlockedException $e) {
error_log("#{$e->errorCode} {$e->apiMessage}");
if ($e->brandedMessage !== null) {
error_log("server brand text: {$e->brandedMessage}");
}
}

When the server returns code 51 (deprecated parameter) with a usable result, the SDK emits a PHP-native deprecation notice via trigger_error($message, E_USER_DEPRECATED), mirrors it on the optional PSR-3 logger at warning level, and returns the result as if the call had succeeded. Code 51 with no result raises AudDInvalidRequestException.

Retry behavior​

Each endpoint is classified into one of three retry classes:

  • READ (streams()->list, streams()->getCallbackUrl, longpolls) — retries on any GuzzleHttp\Exception\TransferException, plus HTTP 408/429/5xx.
  • RECOGNITION (recognize, recognizeEnterprise, advanced()->*) — retries only on pre-upload connection errors 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 errors; 5xx is surfaced because the side effect may have already happened.

Defaults: maxRetries: 3, backoffFactor: 0.5. Backoff is min(backoffFactor * 2**attempt, 30 s) with 0.5x..1.5x jitter. Override per client:

$audd = new AudD('your-token', maxRetries: 5, backoffFactor: 1.0);
$audd = new AudD('your-token', maxRetries: 1); // disable retries

Configuration​

Timeouts​

Defaults: standard endpoint 30 s connect / 60 s read. Enterprise endpoint uses 60 minutes for read because long files take that long to fingerprint server-side; the SDK enables TCP keepalive on long-read layers to survive NAT idle-eviction. Override per call on recognize() and recognizeEnterprise():

$audd->recognize('https://audd.tech/example.mp3', timeout: 10.0); // 10 s read

The override applies only to read; the connect timeout stays at the layer default. For finer-grained control, inject a custom HTTP client.

Custom HTTP client​

Inject any PSR-18 ClientInterface for proxies, custom CA bundles, mTLS, or shared connection pools. Guzzle is the default and the lowest-impedance integration:

use AudD\AudD;
use GuzzleHttp\Client;

$audd = new AudD(
'your-api-token',
httpClient: new Client([
'proxy' => 'http://corp-proxy:8080',
'verify' => '/etc/ssl/internal-ca.pem',
]),
);

The SDK uses the same injected client for both standard and enterprise endpoints, and doesn't close clients it didn't open. Symfony's PSR-18 adapter, Buzz, and php-http's curl client all work.

Observability​

The constructor accepts any PSR-3 LoggerInterface. For structured per-request observability, pass an onEvent hook:

use AudD\AudD;
use AudD\AudDEvent;
use AudD\AudDEventKind;

$audd = new AudD(
'your-api-token',
onEvent: function (AudDEvent $e): void {
if ($e->kind === AudDEventKind::Response) {
error_log("audd {$e->method} -> {$e->httpStatus} ({$e->elapsedMs}ms)");
}
},
);

AudDEvent is a readonly class with kind (Request, Response, Exception), method, url, requestId, httpStatus, elapsedMs, errorCode, and a free-form extras array. Events never carry the api_token or request body. Hook exceptions are swallowed (logged at debug on the configured PSR-3 logger) so observability cannot break the request path.

Token rotation​

Covered under Authentication — $audd->setApiToken('new-token') swaps the token for subsequent calls. PHP is request-scoped, so there's no concurrency to coordinate with.

Retries​

Covered under Retry behavior. maxRetries: and backoffFactor: on the constructor.

Calling endpoints the SDK doesn't wrap​

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 an associative array, runs through the same retry policy, and raises AudDSerializationException 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.

$body = $audd->advanced()->rawRequest('getLinks', ['audd_id' => 12345]);

rawRequest() does not automatically convert status: error bodies into typed exceptions — callers handling untyped endpoints inspect $body['status'] themselves.

Concurrency and lifecycle​

PHP is request-scoped: one client serves one process. There is no async surface, no thread pool. The recommended pattern is one AudD instance per request — instantiate at the top of the controller / handler, let it fall out of scope at the end. __destruct calls close() automatically; PHP reclaims everything at request end either way.

Cancellation does not refund server-side metering

If the PHP process is killed mid-upload, or if a tight timeout: aborts a call after the server already accepted the upload, you can still be billed for that recognition. Server-side metering is outside the SDK's control.


dashboard.audd.io · HTTP API reference · Other SDKs