npmjs.com ↗
Installnpm install signalbird

signalbird - 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.

text
project  →  penyu.io                    (owns the key)
channel  →  critical, info, deploy…     (the notification rule lives here)
event    →  a single record

Install

bash
npm install signalbird

Node 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.

bash
# .env
SIGNALBIRD_DOMAIN_KEY=sb_secret_live_xxxxxxxxxxxxxxxxxxxxxxxx
SIGNALBIRD_SOURCE=api-01     # optional: which server it came from

This key is secret and cannot be embedded in a browser. The server rejects a secret key on any request that carries an Origin header (SECRET_KEY_IN_BROWSER). For the browser use the Browser SDK and a public key.

Quick start

typescript
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:

typescript
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:

typescript
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

typescript
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

typescript
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.

typescript
// 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: false and your application keeps running. Shipping a log is not a valid reason to break a payment flow. In development, set throwOnError: 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 dedupe window 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.

typescript
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):

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.

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:

bash
npm i signalbird@latest

Error 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
typescript
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({ /* … */ })           // Messaging

Radio 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.