PHP SDK

broadcast-php is the official PHP client for the Broadcast API. It covers every API endpoint and works against any Broadcast instance.

Requires PHP 8.1+ with the curl and json extensions. The only Composer dependency is psr/log, so it drops into a Laravel app, a WordPress plugin, or a plain script without pulling in a stack of transitive packages.

Installation

composer require broadcast/broadcast-php

Quick Start

use Broadcast\Client;

$client = new Client([
    'apiToken' => getenv('BROADCAST_API_TOKEN'),
    'host' => 'https://mail.example.com',
]);

$client->subscribers->create(['email' => 'ada@example.com', 'first_name' => 'Ada']);

Warning

host is required — there is no default. Broadcast is self-hosted first, so every instance lives at its own domain. A built-in guess would be wrong for almost everyone, so the client asks you to be explicit.

You can also set BROADCAST_HOST and BROADCAST_API_TOKEN in the environment. These are the same names the Agents CLI uses in ~/.config/broadcast/config.

Create your API token under Settings → API Keys, granting only the permissions your integration needs. See API Authentication.

Configuration

$client = new Client([
    'apiToken' => '...',
    'host' => 'https://mail.example.com',
    'timeout' => 30,             // read timeout, seconds
    'openTimeout' => 10,
    'retryAttempts' => 3,
    'retryDelay' => 1,           // base backoff, multiplied by attempt number
    'maxRetryDelay' => 30,       // ceiling on a server-supplied Retry-After
    'warningsMode' => 'log',     // 'log' | 'raise' | 'ignore'
    'logger' => $psr3Logger,
    'broadcastChannelId' => 42,  // admin/system tokens only
]);

A typo in an option name throws ConfigurationException rather than being silently ignored, so a misspelled timout fails immediately instead of leaving you wondering why the timeout had no effect.

Timeouts, rate-limited requests, and server errors are retried with backoff. A validation error (422) is never retried — it is deterministic, so a retry only adds latency.

Responses

A response behaves like the parsed JSON body, so $result['id'] works as you would expect. Transport metadata is available through methods, which keeps a body field named status reachable as $result['status']:

$result = $client->subscribers->create(['email' => 'ada@example.com']);

$result['id'];                     // the response body
$result->status();                 // 201
$result->warnings();               // parsed warnings, if any
$result->rateLimit()?->remaining;  // requests left in the window
$result->isIdempotentReplay();

Warnings

A successful response can still carry warnings — the API accepted your request but ignored part of it, such as an unrecognised parameter or a filter it could not parse. A mistyped created_after silently widens a result set rather than failing.

foreach ($result->warnings() as $warning) {
    error_log((string) $warning);
}

Set 'warningsMode' => 'raise' to turn them into exceptions instead. Note the write has already happened by then — nothing is rolled back. See API Response Warnings.

Rate Limits

Every response carries the current limit state, and 429s are retried automatically, honouring the server’s Retry-After (capped at maxRetryDelay).

$result = $client->subscribers->list();

$result->rateLimit()->limit;      // 120
$result->rateLimit()->remaining;  // 118
$result->rateLimit()->reset;      // DateTimeImmutable

if ($result->rateLimit()?->remaining < 10) {
    sleep(1);
}

If the retries are exhausted you get a RateLimitException, which carries ->retryAfter so you can requeue the job sensibly.

Common Tasks

Subscribers

$client->subscribers->list(['page' => 1, 'is_active' => true, 'tags' => ['vip']]);
$client->subscribers->find('ada@example.com');
$client->subscribers->create(['email' => 'ada@example.com', 'tags' => ['vip']]);
$client->subscribers->update('ada@example.com', ['first_name' => 'Ada']);
$client->subscribers->addTags('ada@example.com', ['beta']);
$client->subscribers->unsubscribe('ada@example.com');

See Subscribers API for every filter and field.

Transactional email

$client->transactionals->create([
    'to' => 'ada@example.com',
    'subject' => 'Your receipt',
    'body' => '<p>Thanks for your order.</p>',
    'idempotency_key' => "receipt-{$order->id}",
]);

Passing an idempotency_key makes a retry safe: the server stores the response for 24 hours and replays it rather than sending a second email. Call $result->isIdempotentReplay() to tell a replay from a fresh send.

Full details in Transactional Email API.

Broadcasts

$broadcast = $client->broadcasts->create([
    'subject' => 'Weekly update',
    'body' => '<p>Hello</p>',
]);

$client->broadcasts->schedule($broadcast['id'], '2026-08-01T09:00:00Z', 'UTC');

Warning

$client->broadcasts->send($id) sends immediately to the whole audience. There is no unsend. Call $client->status() first — if readiness.broadcasts is false, the channel has no usable email server or sender identity.

Channel Scoping

Admin (system) tokens can address any channel. Scope a callable to one:

$client->withChannel(123, fn () => $client->emailServers->list());

The previous scope is restored afterwards, including when the callable throws. Regular tokens are already channel-scoped and need none of this.

Webhooks

use Broadcast\Webhook;

$payload = file_get_contents('php://input');

$valid = Webhook::verify(
    $payload,
    $_SERVER['HTTP_X_BROADCAST_SIGNATURE'] ?? null,
    $_SERVER['HTTP_X_BROADCAST_TIMESTAMP'] ?? null,
    getenv('BROADCAST_WEBHOOK_SECRET') ?: null
);

if (!$valid) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);
// handle $event['type']

Pass the raw request body. Re-encoding a decoded array changes the bytes and verification will fail. Webhook::eventTypes() lists every event name.

See Webhook Endpoints API for creating and managing endpoints.

Using Your Own HTTP Client

The client ships a cURL transport so installation stays dependency-free. To route requests through Guzzle, Symfony HttpClient, or a PSR-18 client, implement Broadcast\HttpClientInterface — it has a single method — and pass it in:

$client = new Client([
    'apiToken' => '...',
    'host' => 'https://mail.example.com',
    'httpClient' => new MyPsr18Adapter($psr18Client),
]);

Export & Migration

Admin (system) tokens can read the migration endpoints, which back instance backups and moves:

$client = new Client([
    'apiToken' => '...',
    'host' => 'https://mail.example.com',
    'broadcastChannelId' => 42,
]);

$client->migration->manifest();   // sizes the export first

foreach ($client->migration->eachRecord('subscribers') as $subscriber) {
    // ...
}

eachRecord pages automatically and stops when the server reports there is no more data. See Backup & Recovery.

Errors

use Broadcast\Exception\{ApiException, AuthorizationException, RateLimitException, ValidationException};

try {
    $client->subscribers->create(['email' => 'invalid']);
} catch (ValidationException $e) {      // 422 — bad input, do not retry
} catch (AuthorizationException $e) {   // 403 — token lacks permission
} catch (RateLimitException $e) {       // 429
    sleep($e->retryAfter ?? 5);
} catch (ApiException $e) {             // 5xx and other transport failures
}

ValidationException deliberately does not extend ApiException, so catching ApiException for transport problems will not silently swallow a validation error.

Source and Support

The client is open source and its README documents every method:

Prefer Ruby? See the Ruby SDK.

Was this page helpful?

Thanks for your feedback!

Thanks for letting us know. We'll work on improving this page.