composer require pariette/signalbirdpariette/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
composer require pariette/signalbirdPHP 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.
# .env
SIGNALBIRD_DOMAIN_KEY=sb_secret_live_xxxxxxxxxxxxxxxxxxxxxxxx
SIGNALBIRD_SOURCE=api-01Publish the configuration if you want it:
php artisan vendor:publish --tag=signalbird-configQuick start
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:
'signalbird' => [
'driver' => 'monolog',
'handler' => \Signalbird\Sdk\SignalbirdLogHandler::class,
'with' => ['channel' => 'laravel'],
'level' => 'error',
],.env:
LOG_STACK=single,signalbirdMonolog 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:
// config/mail.php
'signalbird' => ['transport' => 'signalbird', 'class' => 'transactional'],MAIL_MAILER=signalbirdMail::to($user)->send(new OrderNotification($order)); // now via SignalbirdSends whose body lives in the panel: template, variables, sender name:
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
// 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
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
use Signalbird\Sdk\Signalbird;
Signalbird::configure('sb_secret_live_…');
Signalbird::error('api', 'the database connection dropped');Or the client directly:
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.
use Signalbird\Sdk\Facades\Signalbird;
$hash = Signalbird::identityHash((string) $user->id); // same as SignalbirdClient::identityHashIt lives on the Radio client, which already holds the secret key; no extra setup. The algorithm (same output in every language):
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.
<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, setSIGNALBIRD_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:
composer update pariette/signalbirdError 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 |
use Signalbird\Sdk\Facades\Signalbird;
Signalbird::critical('critical', 'the payment service died'); // Radio
Signalbird::messaging()->sendEmail([...]); // Messaging
Signalbird::management()->createModuleKey('logger', ['name' => 'payment-service']); // ManagementSIGNALBIRD_DOMAIN_KEY=sb_secret_live_… # Radio
SIGNALBIRD_DOMAIN_KEY=sb_… # Management (falls back to SIGNALBIRD_DOMAIN_KEY)
SIGNALBIRD_DOMAIN_KEY=sb_… # MessagingAll three clients are registered as singletons in the service provider, so you can inject them directly as well:
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.