# PHP SDK

`messagebird/sdk` is the official PHP SDK for the Bird API: a typed, hand-written surface over a generated wire layer. It targets PHP 8.2+ and is synchronous; HTTP goes through any [PSR-18](https://www.php-fig.org/psr/psr-18/) client you already have (Guzzle, Symfony HttpClient, …), discovered automatically. This page covers the client itself; to send email end to end, start with the [PHP email quickstart](/docs/get-started/quickstarts/php/email).

## Install

```bash
composer require messagebird/sdk
```

## Construct a client

<!-- bird:snippet client.options -->

```php
$bird = new Bird(
    getenv('BIRD_API_KEY') ?: '',
    region: 'eu1',                                          // optional — overrides the region inferred from the key prefix
    maxRetries: 2,                                          // retry budget for transient failures (default 2)
    email: new EmailDefaults(from: 'hello@acme.com'),       // optional channel defaults, e.g. a default `from`
    webhookSecret: getenv('BIRD_WEBHOOK_SECRET') ?: null,   // signing secret for $bird->webhooks->unwrap()
);
```

Only the API key is required. The region is inferred from the key's `bk_{region}_` prefix (a `bk_eu1_…` key routes to `https://eu1.platform.bird.com`), so most clients are constructed with the key alone; see [region inference](/docs/sdks/concepts#region-inference) for the rules. `baseUrl` overrides the region entirely (local or self-hosted). Channel defaults set here (for example `email: new EmailDefaults(from: "hello@acme.com")`) make that field optional on every send, and `webhookSecret` is the secret `$bird->webhooks->unwrap()` verifies with. A per-request timeout is configured on the PSR-18 client you pass, not on `Bird`. PSR-18 has no portable timeout, so the transport owns it.

## First call

<!-- bird:snippet email.send -->

```php
$message = $bird->email->send(
    from: 'Bird <onboarding@messagebird.dev>',
    to: ['delivered@messagebird.dev'],
    subject: 'Hello from Bird',
    html: '<p>My first Bird email.</p>',
);
echo $message->getId(), ' ', $message->getStatus();
```

The call returns the API result directly: here an email message with its `em_*` id. For a runnable walkthrough, follow the [PHP quickstart](/docs/get-started/quickstarts/php/email).

## Two-layer design

The SDK is a generated layer plus a hand-owned one. The wire types under `MessageBird\Wire` are generated from Bird's OpenAPI spec (via jane-php), so request and response shapes are always contract-accurate; the surface you call (`$bird->email->send(...)`, the retry loop, idempotency, pagination, errors, webhook verification) is hand-written on top. Wire fields pass through verbatim in `snake_case` (`category`, `created_at`); only SDK-defined identifiers (method names, option names like `idempotencyKey`) are `camelCase`. The same architecture is shared by the [TypeScript](/docs/sdks/typescript), [Go](/docs/sdks/go), and [Python](/docs/sdks/python) SDKs; see [SDK concepts](/docs/sdks/concepts).

## Automatic idempotency and retries

Every mutation (POST, PATCH, DELETE) gets an auto-generated `Idempotency-Key` header, and that one key is reused across every retry attempt, so a retried send can never deliver twice. Retries are on by default (`maxRetries: 2`): the client retries transient failures (429, 5xx except 501, and PSR-18 transport errors) with jittered exponential backoff, honoring the server's `Retry-After` header. Deterministic failures (4xx like 401, 404, 422) are never retried. Override either per call:

<!-- bird:snippet email.options -->

```php
$bird->email->send(
    from: 'Bird <onboarding@messagebird.dev>',
    to: ['delivered@messagebird.dev'],
    subject: 'Hello from Bird',
    html: '<p>My first Bird email.</p>',
    options: new RequestOptions(idempotencyKey: 'order-1234', maxRetries: 0),
);
```

## Errors

A failed call throws. `MessageBird\Exception\ApiException` covers every error response from the server and carries `status` (the HTTP status), `type` (the coarse category), and `errorCode` (the stable `E#####` code). A transport failure that exhausts the retry budget throws `MessageBird\Exception\ConnectionException` instead: there is no HTTP response to report. Both extend `MessageBird\Exception\BirdException`, so catch that to handle either.

<!-- bird:snippet email.errors -->

```php
try {
    $bird->email->send(
        from: 'Bird <onboarding@messagebird.dev>',
        to: ['delivered@messagebird.dev'],
        subject: 'Hello from Bird',
        html: '<p>My first Bird email.</p>',
    );
} catch (ApiException $e) {
    // The server returned an error response. $status is the HTTP status, $type
    // the coarse category, $errorCode the stable E##### code.
    echo $e->status, ' ', $e->errorCode ?? $e->type ?? 'error';
} catch (ConnectionException $e) {
    // The transport failed and the retry budget was exhausted — no HTTP response.
    echo 'transport error: ', $e->getMessage();
}
```

## Pagination

List methods return a lazy `Core\Page`; iterating it with `foreach` auto-paginates across cursors, fetching pages on demand:

<!-- bird:snippet email.list.iterate -->

```php
foreach ($bird->email->list(['status' => 'delivered']) as $message) {
    echo $message->getId(), "\n";
}
```

For manual cursor control, `fetch()` returns a single page: its `data` plus the forward `nextCursor`, which you pass back as `starting_after` to advance:

<!-- bird:snippet email.list.paginate -->

```php
$page = $bird->email->list(['status' => 'delivered'])->fetch();
foreach ($page->data as $message) {
    echo $message->getId(), "\n";
}
$next = $page->nextCursor; // pass back as starting_after to fetch the next page
```

## Escape hatch

Endpoints not yet on the typed surface are reachable through `$bird->get` / `post` / `put` / `patch` / `delete`, with the same auth, retries, idempotency, and base-URL handling:

<!-- bird:snippet client.verbs -->

```php
// The verb methods (get/post/put/patch/delete) run through the same auth,
// retries, idempotency, and base-URL handling as the typed methods; pass a
// path and, for writes, a body array.
$messages = $bird->get('/v1/sms/messages', query: ['limit' => 10]);
$created = $bird->post('/v1/sms/messages', body: [
    'to' => '+15551234567',
    'text' => 'Your code is 123456.',
    'category' => 'authentication',
]);
```

Find the paths in the [API reference](/docs/api).

## Webhooks

Verify a delivered webhook's [Standard Webhooks](https://www.standardwebhooks.com/) signature and get the decoded event. Set the signing secret on the client (or pass it per call), and **pass the raw request body**: the signature is over the raw bytes, so parsing before verifying is the classic webhook bug.

<!-- bird:snippet webhook.unwrap -->

```php
// In your web handler: pass the RAW request body — the signature is over the
// raw bytes, so parsing before verifying is the classic webhook bug.
$rawBody = file_get_contents('php://input') ?: '';
try {
    $event = $bird->webhooks->unwrap($rawBody, getallheaders());
    // $event is the decoded payload as an array; branch on $event['type'].
    echo $event['type'];
} catch (WebhookVerificationError) {
    http_response_code(400); // bad signature, stale timestamp, or missing/malformed headers
}
```

## Next steps

- [PHP email quickstart](/docs/get-started/quickstarts/php/email): a runnable end-to-end send.
- [SDK concepts](/docs/sdks/concepts): the retry, pagination, and region model shared across SDKs.
- [API reference](/docs/api): every operation, with PHP examples.