The official PHP SDK for the Bird platform — email, SMS, WhatsApp, verification, and realtime, over one typed client.
PHP 8.2 or newer. Any PSR-18 HTTP client (Guzzle, Symfony HttpClient, …) is discovered automatically — you do not need to configure one.
composer require messagebird/sdkuse MessageBird\Bird;
$bird = new Bird(getenv('BIRD_API_KEY'));
$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 API key resolves the region automatically (bk_{region}_…). Pass a $baseUrl to target a specific endpoint, or your own PSR-18 client to override transport — see Configuration.
Runnable versions of these live in examples/: quickstart-email.php, quickstart-whatsapp.php.
Set common send fields once, on the client, instead of on every call. Any field left unset on a send() / sendBatch() falls back to the default; a value passed to the send always wins.
use MessageBird\Bird;
use MessageBird\EmailDefaults;
$bird = new Bird(getenv('BIRD_API_KEY'), email: new EmailDefaults(
from: 'Bird <onboarding@messagebird.dev>',
replyTo: ['support@messagebird.dev'],
category: 'transactional',
));
// `from` and `category` are filled from the defaults; both stay optional here.
$bird->email->send(to: ['delivered@messagebird.dev'], subject: 'Hi', html: '<p>Hi.</p>');// Send
$message = $bird->email->send(
from: 'Bird <onboarding@messagebird.dev>',
to: ['delivered@messagebird.dev'],
subject: 'Hello from Bird',
html: '<p>My first Bird email.</p>',
);
// Send a batch — one result per message, in submission order
use MessageBird\Wire\Model\EmailMessageSendRequest;
$batch = $bird->email->sendBatch([
(new EmailMessageSendRequest())->setFrom('onboarding@messagebird.dev')
->setTo(['delivered@messagebird.dev'])->setSubject('Hi')->setHtml('<p>Hi.</p>'),
]);
// Fetch
$message = $bird->email->get('eml_01krdgeqcxet5s7t44vh8rt9mg');
// List — iterating the page auto-paginates across cursors
foreach ($bird->email->list(['status' => 'delivered']) as $message) {
echo $message->getId(), "\n";
}
// Or read one page at a time and advance manually with the cursor
$page = $bird->email->list(['status' => 'delivered'])->fetch();
$page->data; // this page's messages
$page->nextCursor; // pass back as starting_after; null on the last pageAn address (from/to/cc/bcc/replyTo) accepts a plain string ("jane@x.com"), an RFC 5322 mailbox ("Jane <jane@x.com>"), an ['email' => …, 'name' => …] array, or an EmailAddress model — all normalize to the same wire object.
// Send — free-text (with a category) or a stored template
$message = $bird->sms->send(
to: '+15551234567',
text: 'Your verification code is 123456.',
category: 'authentication',
);
// Send by template (id `smt_…` or name), filling its variables
$bird->sms->send(to: '+15551234567', template: 'bird_otp', parameters: ['code' => '123456']);
// Fetch / list (auto-paginates)
$message = $bird->sms->get('sms_01krdgeqcxet5s7t44vh8rt9mg');
foreach ($bird->sms->list(['direction' => 'outbound']) as $message) {
echo $message->getId(), ' ', $message->getStatus(), "\n";
}// Send a template message
$message = $bird->whatsapp->send(
to: '+15551234567',
template: 'bird_otp',
language: 'en',
);
// Fetch / list, and read one message's delivery timeline
$message = $bird->whatsapp->get('wamid_01krdgeqcxet5s7t44vh8rt9mg');
$events = $bird->whatsapp->listEvents('wamid_01krdgeqcxet5s7t44vh8rt9mg');
foreach ($events->getData() ?? [] as $event) {
echo $event->getType(), "\n";
}A two-step flow: start a verification (Bird sends a one-time passcode), then check the code the recipient submits.
use MessageBird\Wire\Model\VerificationCheckRequest;
use MessageBird\Wire\Model\VerificationCreateRequest;
use MessageBird\Wire\Model\VerificationTo;
$verification = $bird->verify->verifications->create(
(new VerificationCreateRequest())->setTo((new VerificationTo())->setPhoneNumber('+15551234567')),
);
$result = $bird->verify->verifications->check(
(new VerificationCheckRequest())
->setTo((new VerificationTo())->setPhoneNumber('+15551234567'))
->setCode('123456'),
);
echo $result->getSuccess() ? 'verified' : 'failed';Publish events to a Realtime app's channels and inspect its live channels and members. Every call authenticates with the app's own key/secret (shown once at creation) on top of the workspace API key — set them as client config, or pass a per-call override to reach a second app. Each method takes the app id (rap_…) first.
use MessageBird\Bird;
use MessageBird\RealtimeOptions;
use MessageBird\Wire\Model\RealtimePublish;
$bird = new Bird(getenv('BIRD_API_KEY'), realtime: new RealtimeOptions(
key: getenv('BIRD_REALTIME_KEY'),
secret: getenv('BIRD_REALTIME_SECRET'),
));
$bird->realtime->publish('rap_...', (new RealtimePublish())
->setEvent('message.created')
->setChannels(['room-42'])
->setData(['text' => 'Hello, room!']));
foreach ($bird->realtime->channels->list('rap_...', ['prefix' => 'room-'])->getData() ?? [] as $channel) {
echo $channel->getName(), ' ', $channel->getMemberCount(), "\n";
}
$bird->realtime->members->disconnect('rap_...', 'usr_...');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.
use MessageBird\Bird;
use MessageBird\Exception\WebhookVerificationError;
$bird = new Bird(getenv('BIRD_API_KEY'), webhookSecret: getenv('BIRD_WEBHOOK_SECRET'));
// In your web handler — $rawBody is the unparsed request body.
try {
$event = $bird->webhooks->unwrap($rawBody, getallheaders());
switch ($event['type']) {
case 'email.delivered':
markDelivered($event['data']['email_id'], $event['data']['recipient']);
break;
case 'email.bounced':
case 'email.complained':
suppress($event['data']['recipient']);
break;
default:
// unknown future event types — an older SDK won't break on a new one
}
} catch (WebhookVerificationError $e) {
http_response_code(400); // bad signature, stale timestamp, or missing/malformed headers
}Transient failures — a 429, a 5xx (except 501), or a PSR-18 transport error — are retried with jittered exponential backoff that honors Retry-After. A single idempotency key is generated once per call and reused across attempts, so a retried write never double-applies. The budget defaults to 2 retries; override it per client or per call:
$bird = new Bird(getenv('BIRD_API_KEY'), maxRetries: 4);
use MessageBird\RequestOptions;
$bird->email->send(to: ['a@b.com'], subject: 'Hi', html: '<p>Hi.</p>',
options: new RequestOptions(maxRetries: 0));A per-request timeout is the injected PSR-18 client's responsibility (PSR-18 has no portable timeout) — configure it on the client you pass to Bird, e.g. a Guzzle client with a timeout.
Every failure is a MessageBird\Exception\BirdException:
ApiException— the server returned a 4xx/5xx. Carries$status(HTTP code),$type, and$errorCodefrom the error body.ConnectionException— the transport failed past the retry budget.
use MessageBird\Exception\ApiException;
use MessageBird\Exception\BirdException;
try {
$bird->email->send(to: ['a@b.com'], subject: 'Hi', html: '<p>Hi.</p>');
} catch (ApiException $e) {
echo $e->status, ' ', $e->errorCode ?? '', ' ', $e->getMessage();
} catch (BirdException $e) {
// transport error, or any other SDK-level failure
}Every operation the API exposes is reachable even before it has a typed method, via the verb methods on the client — get, post, put, patch, delete. They take a leading-slash path on the configured origin (a path that would move the request off-origin is rejected before the key is attached) and return the decoded response.
$data = $bird->get('/v1/email/messages', query: ['status' => 'delivered']);
$bird->post('/v1/some/new/endpoint', body: ['field' => 'value']);new Bird(
apiKey: 'bk_live_…', // resolves the region from the bk_{region}_ prefix
baseUrl: null, // override the resolved endpoint
httpClient: null, // any PSR-18 client; discovered when null
region: null, // resolve a region for a key without a prefix
email: null, // EmailDefaults (see above)
maxRetries: 2, // retry budget for transient failures
);MIT