npm install signalbirdsignalbird - Node.js / TypeScript
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), what happens during quiet hours and how many times the same message may raise an alert all live in the panel, in the channel settings. None of that is in your code, and none of it should be: changing a notification rule must not require redeploying your application.
project → penyu.io (owns the key)
channel → critical, info, deploy… (the notification rule lives here)
event → a single recordInstall
npm install signalbirdNode 18+ is required. The package has no dependencies: it uses Node's
built-in fetch.
The key
Open a project in the panel with Radio → Projects → New project. The server
key (sb_secret_live_…) is shown to you once; only its digest is stored in the
database, so once you close the dialog it cannot be read again, only rotated.
# .env
SIGNALBIRD_DOMAIN_KEY=sb_secret_live_xxxxxxxxxxxxxxxxxxxxxxxx
SIGNALBIRD_SOURCE=api-01 # optional: which server it came fromThis key is secret and cannot be embedded in a browser. The server rejects a secret key on any request that carries an
Originheader (SECRET_KEY_IN_BROWSER). For the browser use the Browser SDK and a public key.
Quick start
import { signalbird } from 'signalbird'
await signalbird().critical('critical', 'the payment service is not responding', {
service: 'iyzico',
attempt: 3,
})
await signalbird().info('info', 'ahmet@x.com created a new account')signalbird() is built from the SIGNALBIRD_DOMAIN_KEY environment variable and is a
singleton.
If you would rather build your own client:
import { SignalbirdClient } from 'signalbird'
const sb = new SignalbirdClient({
domainKey: process.env.SIGNALBIRD_DOMAIN_KEY!,
source: 'api-01',
timeout: 5000,
throwOnError: false, // keep this off in production
})Methods
| Method | Description |
|---|---|
log({ channel, message, level?, context?, source? }) |
The base call |
debugLog(channel, message, context?) |
debug level |
info(channel, message, context?) |
info level |
warn(channel, message, context?) |
warn level |
error(channel, message, context?) |
error level |
critical(channel, message, context?) |
critical level |
batch(events) |
At most 100 records |
captureUncaught(channel?) |
Hooks uncaught errors |
All of them return { ok, eventId?, code?, status? }.
The level overrides the channel default
If you do not send a level, the channel's own level applies. The client
does not invent a default here.
Batch sending
Partial success is normal (the quota can run out halfway through), so the result is not one status but a row-by-row report:
const result = await signalbird().batch([
{ key: 'info', message: 'job 1 finished' },
{ key: 'info', message: 'job 2 finished' },
])
// { accepted: 2, total: 2, results: { 0: { ok: true, eventId: 'rev_…' }, … } }Failed rows are not retried automatically: writing the same log twice also costs something, so the caller makes that decision.
Uncaught errors
signalbird().captureUncaught('critical')It installs the hook but still lets the process die: keeping a process
alive after uncaughtException means running an application that is in a
broken state, and shipping a log does not justify that.
Express
import express from 'express'
import { signalbird } from 'signalbird'
const app = express()
signalbird().captureUncaught('critical')
app.use(async (err, req, res, next) => {
await signalbird().error('api', err.message, {
method: req.method,
path: req.path,
})
res.status(500).json({ error: 'Internal Server Error' })
})Next.js
On the server side (route handler, server action, app/api/**) you use it
directly. Do not use it in client components: the key would be bundled and
shipped to the browser.
// app/api/webhook/route.ts
import { signalbird } from 'signalbird'
export async function POST(req: Request) {
try {
// …
} catch (error) {
await signalbird().error('webhook', (error as Error).message)
throw error
}
}Rules of behaviour
- Silent failure is the default. If Radio cannot be reached the call returns
ok: falseand your application keeps running. Shipping a log is not a valid reason to break a payment flow. In development, setthrowOnError: true. - An unknown channel is not dropped. The first
log('payment-error', …)call opens the channel by itself and it appears in the panel marked "opened automatically". A new channel is silent: the team writes its rule. - The critical level pierces quiet hours.
- Deduplication silences the notification, not the record. If the same
message arrives again within the channel's
dedupewindow the second notification is not sent, but the record is kept.
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.
import { signalbird } from 'signalbird'
const hash = signalbird().identityHash(user.id) // new SignalbirdClient({ domainKey }).identityHash(...)It 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.
Version notice
The SDK reports its version on every request: X-Signalbird-Sdk: node/<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 (console.warn).
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:
npm i signalbird@latestError codes
| Code | Meaning |
|---|---|
INVALID_KEY |
The key is missing, wrong, or the project is inactive |
SECRET_KEY_IN_BROWSER |
A secret key was used from a browser |
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 three more surfaces:
| Surface | Entry point | What it does |
|---|---|---|
| Messaging | SignalbirdMessaging |
email, SMS, push; contacts, lists, campaigns; webhook signature |
| Management | SignalbirdManagement, management() |
Radio project/channel, chat inbox, apps and devices |
| App | signalbird/app · /react · /vue · /angular · /react-native |
live chat and push registration for the end user |
import { signalbird, management, SignalbirdMessaging } from 'signalbird'
await signalbird().critical('critical', 'the payment service died') // Radio
await management().createModuleKey('logger', { name: 'payment-service' }) // Management
await new SignalbirdMessaging({ domainKey }).sendEmail({ /* … */ }) // MessagingRadio wants an sb_secret_live_… key, the other two want sb_…. If you hand over a
key from the wrong family you get WRONG_KEY_TYPE while constructing the
client.