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:
- Source: github.com/send-broadcast/broadcast-php
- Packagist: packagist.org/packages/broadcast/broadcast-php
Prefer Ruby? See the Ruby SDK.