Documentation
Sign inGet started

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

Install

Codevoorbeeld
composer require messagebird/sdk

Construct a client

Codevoorbeeld
$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 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

Codevoorbeeld
$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.

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, Go, and Python SDKs; see SDK 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:
Codevoorbeeld
$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.
Codevoorbeeld
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:
Codevoorbeeld
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:
Codevoorbeeld
$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:
Codevoorbeeld
// 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.

Webhooks

Verify a delivered webhook's Standard Webhooks 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.
Codevoorbeeld
// 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