packagist.org ↗
Installcomposer require pariette/signalbird

pariette/signalbird - PHP / Laravel

The Signalbird Radio client. It has exactly one job: writing a message from your project into a channel.

Who the notification reaches, which channel carries it (push/email) and what happens during quiet hours live in the panel, in the channel settings, not in your code.

Install

bash
composer require pariette/signalbird

PHP 8.1+ and ext-curl are required. Guzzle is not: a logging library forcing an HTTP client version on your project is the single most common source of version conflicts.

On Laravel the package is discovered automatically (auto-discovery).

The key

Open a project in the panel with Radio → Projects → New project. The server key (sb_secret_live_…) is shown to you once.

bash
# .env
SIGNALBIRD_DOMAIN_KEY=sb_secret_live_xxxxxxxxxxxxxxxxxxxxxxxx
SIGNALBIRD_SOURCE=api-01

Publish the configuration if you want it:

bash
php artisan vendor:publish --tag=signalbird-config

Quick start

php
use Signalbird\Sdk\Facades\Signalbird;

Signalbird::critical('critical', 'the payment service is not responding', [
    'service' => 'iyzico',
]);

Signalbird::info('info', 'ahmet@x.com created a new account');

Methods

Method Description
log(string $channel, string $message, ?string $level = null, ?array $context = null) The base call
debug · info · warn · error · critical Level shortcuts
batch(array $events) At most 100 records
identityHash(string $externalId): string Identity-verification hash for a chat/push visitor (below)

All of them return an array: ['ok' => true, 'event_id' => 'rev_…'] or ['ok' => false, 'code' => 'LIMIT_REACHED'].

Wiring Laravel logs into Radio

Your existing Log::error() lines land in Radio without writing a single line of code.

config/logging.php:

php
'signalbird' => [
    'driver'  => 'monolog',
    'handler' => \Signalbird\Sdk\SignalbirdLogHandler::class,
    'with'    => ['channel' => 'laravel'],
    'level'   => 'error',
],

.env:

text
LOG_STACK=single,signalbird

Monolog levels are mapped onto Radio's five levels; distinctions such as emergency/alert collapse into critical.

Records accumulate through the request and go out in one batch call at the end: a separate HTTP request per line would take longer than the page itself. That is why pulling level down to debug is possible too, but the framework's own noise (queries, cache) burns your Radio quota fast; in production info or warning is the better choice.

Routing Laravel mail through Signalbird

There are two ways and both end at the same place. The difference is where the body lives.

Your existing Mailables: one line of configuration, no call site changes:

php
// config/mail.php
'signalbird' => ['transport' => 'signalbird', 'class' => 'transactional'],
dotenv
MAIL_MAILER=signalbird
php
Mail::to($user)->send(new OrderNotification($order));   // now via Signalbird

Sends whose body lives in the panel: template, variables, sender name:

php
Signalbird::mail()
    ->to($user->email)
    ->template('Order Confirmation')    // the name shown in the panel, or its id
    ->vars(['name' => $user->name])
    ->fromName('Penyu Support')
    ->replyTo('support@penyu.io')
    ->transactional()                   // REQUIRED: the class has no default
    ->send();

If you do not want to wait for a deploy in order to change the text, take the second one: the template is edited in the panel and the code stays the same.

Two limits: the transport carries no attachments (it does not drop them silently, it errors explicitly) and each recipient gets a separate request: in Signalbird every recipient is a separate record, and the bulk thing is the campaign.

Exception handler

php
// bootstrap/app.php (Laravel 11)
use Signalbird\Sdk\Facades\Signalbird;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->reportable(function (Throwable $e) {
        Signalbird::error('laravel', $e->getMessage(), [
            'exception' => get_class($e),
            'file' => $e->getFile() . ':' . $e->getLine(),
        ]);
    });
})

Queued job

php
use Signalbird\Sdk\Facades\Signalbird;

class ProcessPaymentJob implements ShouldQueue
{
    public function handle(): void
    {
        // ... the payment itself
        Signalbird::info('payment', "Order #{$this->orderId} succeeded");
    }

    public function failed(Throwable $e): void
    {
        Signalbird::critical('critical', "Order #{$this->orderId}: {$e->getMessage()}");
    }
}

PHP without Laravel

php
use Signalbird\Sdk\Signalbird;

Signalbird::configure('sb_secret_live_…');
Signalbird::error('api', 'the database connection dropped');

Or the client directly:

php
use Signalbird\Sdk\SignalbirdClient;

$sb = new SignalbirdClient(
    domainKey: getenv('SIGNALBIRD_DOMAIN_KEY'),
    source: 'api-01',
);

$sb->warn('disk', 'the disk is 85% full');

Visitor identity verification - identityHash

The chat widget and the app SDK introduce a signed-in user by external_id. Because the public key sits in the page, Signalbird trusts that identity only when it comes with a valid hash generated on your server (2.7.0). Without it the visitor is not linked to the contact record, the device is not bound to that user as a push target, and the channel agent's tools see the identity only under visitor.unverified.

php
use Signalbird\Sdk\Facades\Signalbird;

$hash = Signalbird::identityHash((string) $user->id);   // same as SignalbirdClient::identityHash

It lives on the Radio client, which already holds the secret key; no extra setup. The algorithm (same output in every language):

text
identity_hash = hex( HMAC-SHA256( hex(SHA-256(sb_secret_live_…)), external_id ) )

The key is the SHA-256 digest of the secret key (the server never stores the key in plain text). Usage: Widget - identity verification, App SDK.

blade
<script async src="https://signalbird.io/sdk/v1/signalbird.js"
        data-key="sb_public_live_…" data-channel="support"
        data-external-id="{{ $user->id }}"
        data-identity-hash="{{ Signalbird::identityHash((string) $user->id) }}"></script>

Rules of behaviour

  • Silent failure is the default. If Radio cannot be reached the call returns ['ok' => false] and your application keeps running. In development, set SIGNALBIRD_THROW=true.
  • An unknown channel is not dropped. It opens on the first call, appears marked "opened automatically", and is silent: the team writes its rule.
  • The critical level pierces quiet hours.
  • Deduplication silences the notification, not the record.

Version notice

The SDK reports its version on every request: X-Signalbird-Sdk: php/<version>. When a newer version is released the API responds with Signalbird-Sdk-Status: outdated (or unsupported once it is no longer supported) and the SDK writes a warning once per process (error_log). The warning is not an error: the request result does not change.

In the panel, Domains → [domain] → Keys shows which SDK version each key is used with; keys on an old version also get a banner at the top of the panel and a notification. To update:

bash
composer update pariette/signalbird

Error codes

Code Meaning
INVALID_KEY The key is missing, wrong, or the project is inactive
MODULE_DISABLED Your package does not include the Radio (logger) module
LIMIT_REACHED Your monthly record limit is full
CHANNEL_DISABLED The channel is off: nothing is written and no quota is spent

The other surfaces of the package

This page describes Radio. The same package carries two more surfaces:

Surface Access What it does
Messaging Signalbird::messaging() email, SMS, push; contacts, lists, campaigns; webhook signature
Management Signalbird::management() Radio project/channel, chat inbox, apps and devices
php
use Signalbird\Sdk\Facades\Signalbird;

Signalbird::critical('critical', 'the payment service died');                 // Radio
Signalbird::messaging()->sendEmail([...]);                                    // Messaging
Signalbird::management()->createModuleKey('logger', ['name' => 'payment-service']);  // Management
dotenv
SIGNALBIRD_DOMAIN_KEY=sb_secret_live_…            # Radio
SIGNALBIRD_DOMAIN_KEY=sb_…              # Management (falls back to SIGNALBIRD_DOMAIN_KEY)
SIGNALBIRD_DOMAIN_KEY=sb_…        # Messaging

All three clients are registered as singletons in the service provider, so you can inject them directly as well:

php
public function __construct(
    private \Signalbird\Sdk\Management\ManagementClient $signalbird,
) {}

End-user chat and push registration do not exist in PHP: that surface lives on the client side, in the App SDK.