# audd-php


<div className="sdk-page-header-links">
  <a className="button button--primary" href="https://github.com/AudDMusic/audd-php" target="_blank" rel="noopener">View on GitHub</a>
  <a className="button button--secondary" href="https://packagist.org/packages/audd/audd" target="_blank" rel="noopener">Packagist</a>
</div>

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

```bash
composer require audd/audd
```

```php
<?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](https://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](https://dashboard.audd.io).

```php
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:

```php
$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 clip up to 25 seconds / 10 MB and returns a single `RecognitionResult` on a match, or `null` when the clip processed but matched nothing.

```php
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](https://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 broadcasts, podcasts, and full sets longer than 25 seconds, use the enterprise endpoint. It chunks the file server-side and returns every match.

```php
$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()`.

:::warning 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_:` to populate provider sub-objects on the result. Without it, those fields are `null`.

```php
$result = $audd->recognize(
    'https://audd.tech/example.mp3',
    return_: ['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`, `napster`, `musicbrainz`. Each provider adds latency.

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

```php
$result = $audd->recognize($url, return_: ['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](https://github.com/AudDMusic/audd-php#what-you-get-back).

## 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:

- **Callbacks** — AudD POSTs each match (and each lifecycle notification) to a URL you control. See [Receive callbacks from a stream](#receive-callbacks-from-a-stream).
- **Longpoll** — your code polls AudD's `/longpoll/` endpoint and receives matches and notifications synchronously over HTTP. See [Poll for stream events (longpoll)](#poll-for-stream-events-longpoll).

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

```php
$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

```php
$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:

```php
$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
<?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](https://github.com/AudDMusic/audd-php#streams).

## 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.

:::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 `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:

```php
$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.

```php
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):

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

On the client (no api_token needed):

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

:::warning

The custom-catalog endpoint requires special access. Contact [api@audd.io](mailto:api@audd.io) to enable it. Calls without access raise `AudDCustomCatalogAccessException`.

:::

```php
$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

```php
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:

```php
} 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:

```php
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:

```php
$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()`:

```php
$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:

```php
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:

```php
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](#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](#retry-behavior). `maxRetries:` and `backoffFactor:` on the constructor.

### Calling undocumented endpoints

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.

```php
$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.

:::warning 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](https://dashboard.audd.io) · [HTTP API reference](/) · [Other SDKs](/sdks)
