Looking to hire Laravel developers? Try LaraJobs

laravel-bird maintained by spits-online

Description
Bird SMS and WhatsApp for Laravel
Author
Last update
2026/10/09 16:29 (dev-main)
License
Downloads
4
Tags

Comments
comments powered by Disqus

Latest Version on Packagist Tests PHPStan Total Downloads

Notification channels for Bird (formerly MessageBird), plus a small client for the contacts in your workspace.

use Illuminate\Notifications\Notification;
use SpitsOnline\Bird\Channels\SmsChannel;
use SpitsOnline\Bird\Messages\SmsMessage;

class OrderShipped extends Notification
{
    public function via(object $notifiable): array
    {
        return [SmsChannel::class];
    }

    public function toBirdSms(object $notifiable): SmsMessage
    {
        return SmsMessage::create('Your order has shipped!');
    }
}

Requirements

  • PHP 8.3+
  • Laravel 12 or 13
  • A Bird workspace with an SMS and/or WhatsApp channel

Installation

composer require spits-online/laravel-bird

Add your credentials to .env. Only the channels you use need an id.

BIRD_ACCESS_KEY=
BIRD_WORKSPACE_ID=
BIRD_SMS_CHANNEL_ID=
BIRD_WHATSAPP_CHANNEL_ID=

You'll find these in Bird:

  • Access key: Settings → Access keys.
  • Workspace id: Settings → Workspace.
  • Channel ids: Channels → your SMS or WhatsApp channel → Channel ID.

That's all the configuration most apps need, so you don't have to publish a config file. When you do want to change something, such as naming your WhatsApp templates, create config/bird.php with only the keys you change. It's merged over the package defaults key by key, so everything you leave out keeps its default:

// config/bird.php
return [
    'templates' => [
        'order_shipped' => [
            'project_id' => env('BIRD_ORDER_SHIPPED_TEMPLATE'),
            'locale' => 'nl',
        ],
    ],
];

Don't copy keys at their default value. A copied default looks like a deliberate choice, and it stops following the package when the default changes. To see every option, you can publish the full file with php artisan vendor:publish --tag="bird-config". Keep the keys you change and delete the rest.

Sending notifications

Choosing the recipient

The channels send to whatever routeNotificationForBird() on your notifiable returns: a phone number in international format, or an array of them to send to several people.

class User extends Authenticatable
{
    use Notifiable;

    public function routeNotificationForBird(): string
    {
        return $this->phone_number; // international format: +31612345678
    }
}

To send to someone else, set the recipient on the message (->to('+31612345678')), or send an on-demand notification:

Notification::route('bird', '+31612345678')->notify(new OrderShipped);

bird is the route name both Bird channels read, like mail for the mail channel. A phone number is the same for SMS and WhatsApp, so you set it once, and the notification's via() still decides which channels send.

SMS

Add SmsChannel to via() and return an SmsMessage from toBirdSms(), as in the example at the top.

WhatsApp

To start a WhatsApp conversation you need a template approved in Bird Studio. Add WhatsAppChannel to via() and return a WhatsAppMessage from toBirdWhatsApp():

use SpitsOnline\Bird\Channels\WhatsAppChannel;
use SpitsOnline\Bird\Messages\Template;
use SpitsOnline\Bird\Messages\WhatsAppMessage;

public function via(object $notifiable): array
{
    return [WhatsAppChannel::class];
}

public function toBirdWhatsApp(object $notifiable): WhatsAppMessage
{
    return WhatsAppMessage::template(
        Template::create(
            projectId: 'your-project-id',
            parameters: ['name' => $notifiable->first_name],
            locale: 'nl',
        ),
    );
}

The template's parameters are the variables you defined in Bird Studio. Each value is sent with a matching type: a boolean as boolean, an int or float as number, an array as object, and everything else as string. The version defaults to latest; pass version: to pin one.

Naming templates

To avoid repeating project ids, give your templates a name in your app's config/bird.php. Each entry needs a project_id. version (default latest) and locale are optional:

// config/bird.php
return [
    'templates' => [
        'order_shipped' => [
            'project_id' => env('BIRD_ORDER_SHIPPED_TEMPLATE'),
            'locale' => 'nl',
        ],
        'appointment_reminder' => [
            'project_id' => env('BIRD_APPOINTMENT_REMINDER_TEMPLATE'),
            // Pin a version instead of using the latest
            'version' => 'a1b2c3d4-…',
            'locale' => 'en',
        ],
    ],
];

Then use them by name:

return WhatsAppMessage::template(
    Template::named('order_shipped', ['name' => $notifiable->first_name]),
);

Inside WhatsApp's 24-hour service window you can also send any other message body Bird supports:

WhatsAppMessage::body([
    'type' => 'text',
    'text' => ['text' => 'Thanks, we received your reply.'],
]);

SMS and WhatsApp together

A notification can use both channels. Each one calls its own method:

public function via(object $notifiable): array
{
    return [SmsChannel::class, WhatsAppChannel::class];
}

public function toBirdSms(object $notifiable): SmsMessage
{
    return SmsMessage::create('Your order has shipped!');
}

public function toBirdWhatsApp(object $notifiable): WhatsAppMessage
{
    return WhatsAppMessage::template(
        Template::named('order_shipped', ['name' => $notifiable->first_name]),
    );
}

Sending without a notification

use SpitsOnline\Bird\Facades\Bird;
use SpitsOnline\Bird\Messages\SmsMessage;

$sent = Bird::send(
    SmsMessage::create('Your code is 123456')->to('+31612345678'),
);

$sent->id;     // Bird's message id
$sent->status; // "accepted"

Pass a channelId to send through a channel other than the configured one: Bird::send($message, channelId: '…').

Managing contacts

use SpitsOnline\Bird\Facades\Bird;

// Creates the contact, or updates the one with this phone or email
$contact = Bird::contacts()->upsert('+31612345678', [
    'firstName' => 'Jane',
    'lastName' => 'Doe',
]);

$contact = Bird::contacts()->find($contactId);
$contact->displayName;  // "Jane Doe"
$contact->phoneNumber;  // "+31612345678"
$contact->emailAddress; // "jane@example.com" or null
$contact->attributes;   // ['firstName' => 'Jane', ...]

Bird::contacts()->delete($contactId);

Contacts are listed a page at a time:

$page = Bird::contacts()->list(limit: 100);

foreach ($page->contacts as $contact) {
    // ...
}

$next = Bird::contacts()->list(
    limit: 100,
    pageToken: $page->nextPageToken, // null on the last page
);

Error handling

Every exception extends SpitsOnline\Bird\Exceptions\BirdException:

Exception When
RequestFailed Bird returned an error. $status, $errorCode and $body hold what Bird sent back.
ConnectionFailed Bird couldn't be reached.
MissingConfiguration An env value is missing, or Template::named() gets a name that isn't in config/bird.php. The message says what to set.
InvalidRecipient A notification has no recipient.
BirdException A notification uses a channel but doesn't have its toBirdSms() or toBirdWhatsApp() method.
use SpitsOnline\Bird\Exceptions\RequestFailed;

try {
    $contact = Bird::contacts()->find($contactId);
} catch (RequestFailed $e) {
    if ($e->status === 404) {
        // the contact doesn't exist
    }

    throw $e;
}

When a channel throws, Laravel fires its NotificationFailed event. For queued notifications, the job is retried as usual.

Testing your app

Bird::fake() records messages instead of sending them:

use SpitsOnline\Bird\Facades\Bird;
use SpitsOnline\Bird\Messages\Message;

Bird::fake();

$user->notify(new OrderShipped);

Bird::assertSent(function (Message $message) use ($user) {
    return $message->recipients()[0]->value === $user->phone_number;
});
Bird::assertNotSent(fn (Message $message) => /* ... */);
Bird::assertNothingSent();

Contact calls go through Laravel's HTTP client, so fake them with Http::fake().

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently. Upgrading from 1.x? See UPGRADE.

Contributing

Please see CONTRIBUTING for details.

Security vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.