# PHP SDK

> Install and use broadcast-php, the official PHP client for the Broadcast API: subscribers, broadcasts, sequences, transactional email, and webhooks.

Source: https://sendbroadcast.net/docs/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

```bash
composer require broadcast/broadcast-php
```

## Quick Start

```php
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](https://sendbroadcast.net/docs/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](https://sendbroadcast.net/docs/api-authentication).

## Configuration

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

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

```php
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](https://sendbroadcast.net/docs/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`).

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

```php
$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](https://sendbroadcast.net/docs/api-subscribers) for every filter and field.

### Transactional email

```php
$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](https://sendbroadcast.net/docs/api-transactional-email).

### Broadcasts

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

### Suppressions

```php
$client->suppressions->check('ada@example.com');   // will this address receive mail?
$client->suppressions->list(['page' => 1, 'email' => 'example.com']);
$client->suppressions->add('bounced@example.com');
$client->suppressions->remove('bounced@example.com');
$client->suppressions->bulkAdd(['a@example.com', 'b@example.com']);   // up to 10,000
$client->suppressions->bulkRemove(['a@example.com']);
```

`check` reads across both the channel's list and the installation-wide global
list, so it answers the question integrations actually ask: will this address
receive mail? Adding an already-suppressed address is a success (200 rather
than 201), so there is no need to check first.

The global list is a separate resource and needs an admin (system) token:

```php
$client->globalSuppressions->list();
$client->globalSuppressions->add('spamtrap@example.com');
$client->globalSuppressions->remove('spamtrap@example.com');
$client->globalSuppressions->bulkAdd([...]);
$client->globalSuppressions->bulkRemove([...]);
```

See [Suppressions API](https://sendbroadcast.net/docs/api-suppressions) for the full semantics.

### Channel settings and brand kit

```php
$client->channelSettings->get();                   // general, sender, unsubscribe_page, confirmation_page, opt_in_forms
$client->channelSettings->update(['unsubscribe_page' => ['legal_notice_url' => 'https://example.com/impressum']]);

$client->channelDesign->get();                     // the brand kit
$client->channelDesign->update(['colors' => ['accent' => '#ff5500'], 'typography' => ['font' => 'georgia']]);
```

Updates are partial: send only what changes, and the response is the whole object after the change. Read `$result->warnings()` for keys the server ignored. Changing either one needs the Channel Settings write permission. The brand kit switches (`match_brand_kit` in `unsubscribe_page`, `confirmation_page` and `opt_in_forms`) are channel settings, not part of the brand kit update. The brand kit logo is the `logo_url` of an image in the channel's file library. Reading and changing channel settings, and changing the brand kit, need Broadcast 2.46.0 or later and the next release of this SDK after 0.6.0. See [Channel Settings API](https://sendbroadcast.net/docs/api-channel-settings) and [Brand Kit API](https://sendbroadcast.net/docs/api-channel-design).

### Users

Create and manage the people who sign in to Broadcast, and what they can reach. This
needs an **admin (system) token** with the Users permission, and no channel: a channel
token gets `AuthorizationException`.

```php
$client->users->list(['q' => 'ada', 'status' => 'active']);
$user = $client->users->create([
    'email' => 'ada@example.com', 'first_name' => 'Ada', 'last_name' => 'Lovelace',
    'send_password_reset' => true, // or 'password' => '...'
]);
$client->users->get($user['id']); // includes system and channel permissions
$client->users->update($user['id'], ['last_name' => 'King']);
$client->users->deactivate($user['id']);
$client->users->activate($user['id']); // also clears a lockout
$client->users->delete($user['id']);
```

Channel access. A write **replaces** the user's whole record on that channel. Pass
exactly one of `permissions`, `role` or `preset_id`:

```php
$client->users->channelPermissions($user['id']);
$client->users->setChannelPermissions($user['id'], $channelId, ['role' => 'Editor']);
$client->users->setChannelPermissions($user['id'], $channelId, ['permissions' => ['subscribers_read' => true]]);
$client->users->bulkChannelPermissions($user['id'], [3, 4], ['preset_id' => 12]);
$client->users->removeChannelPermissions($user['id'], $channelId);

$client->users->systemPermissions($user['id']);
$client->users->updateSystemPermissions($user['id'], ['user_management' => true]);
```

What to expect:

- **Sudo users are read-only.** Changing one, or their permissions, raises
  `AuthorizationException`, and sudo access can never be granted through the API.
- **Nothing is silently ignored.** A request the API will not apply (such as `sudo`,
  `active`, or permissions inside the user payload) raises `ValidationException` and changes
  nothing. Use `deactivate` and `activate` for sign-in access.
- A bulk apply that applies to no channel raises `ValidationException`; otherwise check its
  `failed` list.
- Passing none, or more than one, of the permission options raises `InvalidArgumentException`
  before any request is sent.

See [Users API](https://sendbroadcast.net/docs/api-users) for every field and rule, and
[Users & Permissions](https://sendbroadcast.net/docs/users-and-permissions) for what the roles and permissions mean.

## Channel Scoping

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

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

```php
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](https://sendbroadcast.net/docs/api-webhook-endpoints) 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:

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

```php
$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) {
    // ...
}

foreach ($client->migration->eachRecord('unsubscribedEmails') as $row) {
    // the channel's own suppression list
}
```

`eachRecord` pages automatically and stops when the server reports there is no more
data. See [Backup & Recovery](https://sendbroadcast.net/docs/backup).

The channel's own suppression list is a separate collection from `suppressions`, which is the global list, so export both. It needs Broadcast 2.40.0 or later and version 0.4.0 or later of this SDK.

## Errors

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

try {
    $client->subscribers->create(['email' => 'invalid']);
} catch (ValidationException $e) {      // 422: bad input, do not retry
} catch (AuthenticationException $e) {  // 401: bad token, or it lacks this permission
} catch (AuthorizationException $e) {   // 403: needs an admin token, or the user is sudo
} 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](https://github.com/send-broadcast/broadcast-php)
- **Packagist:** [packagist.org/packages/broadcast/broadcast-php](https://packagist.org/packages/broadcast/broadcast-php)

Prefer Ruby? See the [Ruby SDK](https://sendbroadcast.net/docs/ruby-sdk).
