signalbird.io/sdk/v1 ↗
Install<script async src="https://signalbird.io/sdk/v1/signalbird.js" data-key="sb_public_live_…" data-channel="destek"></script>

Web Widget - Live chat (signalbird.js)

A live chat bubble you add to your site with a single line. Visitors write, your team replies from Inbox in the panel. The widget also links a visitor to a contact (identify) and registers web push devices (push.register), so the same person can be targeted in campaigns.

Zero dependencies, under 40 KB gzip, runs inside Shadow DOM - your CSS does not leak into the widget and the widget's CSS does not leak into your site.

Install

html
<script async
  src="https://signalbird.io/sdk/v1/signalbird.js"
  data-app-key="sb_public_live_xxxxxxxxxxxxxxxx"></script>

That is all. On load the script calls POST /v1/sdk/bootstrap to fetch the app settings and draws the bubble. Put it right before </body> or in <head> with async; it does not block page load.

Key

In the panel open Inbox → Settings → Apps → New app; you get an app key (sb_public_live_…).

This key is public - it lives in your HTML, anyone can see it. Its safety comes from constraint: requests from outside the allowed origins (domains) in the app settings are rejected with 403. When installing on a new domain (staging, a new site) add it to the allowed origins first.

The secret key (sb_secret_live_…) does not work here; sb_… is secret and must never be embedded in HTML.

Options

Passed via the script tag (data-*) or via Signalbird.init(); both map to the same fields.

data-* init() field Default Description
data-app-key publicKey - Required. Must start with sb_public_live_
data-base-url baseUrl https://signalbird.io/api For self-hosted setups
data-locale locale browser language tr or en
- user - User known at page load (same shape as identify)
data-external-id user.external_id - The signed-in user's id in your system
data-identity-hash identityHash - Hash of external_id - generated on your server (Identity verification)
- debug false Verbose console output

With data-app-key present the widget boots itself; you do not need to call init(). To decide the key at runtime, omit data-app-key and call Signalbird.init({...}).

API

window.Signalbird is ready once the script has loaded. No method ever throws into the host page: a widget problem must not break your site.

Method Description
Signalbird.init({ publicKey, baseUrl?, locale?, user?, identityHash?, debug? }) Manual setup
Signalbird.identify({ external_id, identityHash?, email?, name?, phone?, attributes? }) Links the visitor to a contact - call after login. Without identityHash the identity is treated as unverified
Signalbird.chat.open() · close() · toggle() Opens / closes the panel
Signalbird.chat.isOpen() boolean
Signalbird.chat.on('unread' | 'open' | 'close', fn) · off(event, fn) Event listener. unread is called with the count
Signalbird.push.register({ token, platform, provider?, external_id? }) Adds the device to the push registry. platform: web · ios · android; provider: fcm · apns · webpush
Signalbird.destroy() Removes the widget from the DOM and stops timers

Identifying the user

A visitor starts anonymous (localStorage['sb_visitor']). Call identify when they log in; chat history and the contact record are linked to that user, and your team sees their name in Inbox.

html
<script>
  Signalbird.identify({
    external_id: 'user-1042',          // the id in your own system
    identityHash: '{{ hash }}',        // generated on YOUR server - see below
    email: 'ahmet@example.com',
    name: 'Ahmet Çil',
    attributes: { plan: 'pro', company: 'Penyu' },
  })
</script>

external_id is the stable id in your system; the person stays the same even if the email changes. With the same external_id you can call sendPush({ to: 'external:user-1042' }) from the Messaging SDK.

Opening programmatically

Open the panel from your own "Help" link:

html
<button onclick="Signalbird.chat.open()">Chat with us</button>
javascript
Signalbird.chat.on('unread', (count) => {
  document.querySelector('#help-badge').textContent = count > 0 ? String(count) : ''
})

Web push registration

After you obtain a token with your own service worker, register the device; the contact becomes targetable in push campaigns from the panel.

javascript
const token = await getFcmToken()   // your FCM / Web Push flow
Signalbird.push.register({ token, platform: 'web', provider: 'fcm', external_id: 'user-1042' })

React / Next.js

tsx
'use client'
import Script from 'next/script'

export function SignalbirdWidget({ user }: { user?: { id: string; email: string; name: string } }) {
  return (
    <Script
      src="https://signalbird.io/sdk/v1/signalbird.js"
      data-app-key={process.env.NEXT_PUBLIC_SIGNALBIRD_APP_KEY}
      strategy="afterInteractive"
      onLoad={() => {
        if (user) window.Signalbird?.identify({ external_id: user.id, email: user.email, name: user.name })
      }}
    />
  )
}

A useEffect works too: create a <script> element, append it to document.body, and call window.Signalbird?.identify(...) on its load event. Calling Signalbird.destroy() on unmount is optional; the widget is a singleton and ignores a second init.

Features

  • Launcher bubble + panel; fullscreen at ≤ 640 px
  • The empty state is an invitation, not a form: brand avatar, greeting, response-time promise and quick-start chips derived from your support topics. Tapping a chip sends it as a message - "I don't know what to write" is the most common reason people leave
  • Three ways it makes itself noticed: a greeting card shown once, 11 s in, if the visitor has never opened the panel; a preview of the reply above the bubble while the panel is closed; a quiet pulse on the bubble while anything is unread. All three are dismissible and the choice is stored in the browser
  • Prechat form (name / email - per app settings), greeting, offline banner outside working hours
  • Day separators, ✓ / ✓✓ read receipts, typing indicator both ways
  • Message grouping: consecutive messages from the same sender within 5 min form one block; avatar and time appear only on the last line
  • Emoji picker, a jump to latest button in long threads, links in message bodies are clickable
  • 420 characters per message (served by the API, configurable). A counter appears over the last 60 characters and sending is blocked past the limit. It applies to both sides: what burns tokens is length, not who typed it
  • File uploads: drag & drop, paste; jpeg / png / gif / webp / avif, PDF and plain text - capped at max_attachment_mb. There is deliberately no image/* wildcard: image/svg+xml is a scriptable document, not an image. The type is read from the file's contents server-side; renaming won't help
  • Reply with quote, emoji reactions, edit / delete your own message within 15 min
  • Unread badge + document.title blink, sound
  • Enter sends, Shift+Enter inserts a newline
  • tr / en auto-detected; 1–5 rating on close
  • Live connection (socket.io); falls back to polling when it cannot connect (20 s → 180 s while closed, 3 s while open)
  • The visitor moves and resizes the panel: drag the header, pull the grip in the outer top corner. The choice is stored in the browser (localStorage['sb_geometry']); disabled on mobile
  • Mobile: fullscreen (100dvh), notch and home-bar safe areas, the composer stays above the keyboard, the page behind does not scroll, and the handle at the top closes the panel when dragged down
  • Light / dark theme, from the theme setting or the visitor's own preference (auto)
  • Ending a chat starts a fresh one next time - a closed thread never reopens
  • Shadow DOM isolation, brand colour / logo / position from the panel

Panel settings

There are no appearance options in code; everything lives under Inbox → Settings → Apps and reaches the widget via bootstrap.

Panel setting Effect in the widget
Chat on / off (chat_enabled) When off the widget renders nothing
Allowed origins 403 from any domain not on the list
Theme colour, position (right / left) Brand colour and the panel's starting place
Logo URL (logo_url) Header avatar; the launcher when launcher_icon: logo
Theme (theme: light / dark / visitor's preference) The widget's own surface - never the host page
Launcher icon (launcher_icon: bird / bubble / logo) The mark on the launcher
Launcher behaviour (launcher_mode: always / on request) With manual the launcher is never drawn; your own button opens the chat
Panel layout (layout: corner window / edge drawer / inline) sidebar docks a full-height panel to the edge; inline renders the chat inside the page
Inline container (inline_selector) CSS selector of the element the inline chat renders into (defaults to #signalbird-chat)
Agent display name (agent_display_name) Visitors see this instead of the agent's real name
Per-language texts (texts) Greeting, offline note and launcher label follow the visitor's language
Launcher text, greeting Label next to the bubble, first message
Prechat form fields Whether name / email are required
Working hours Offline banner and "we'll get back to you" note
max_attachment_mb Upload cap

Changing a setting needs no redeploy; it applies on the next page load.

Opening the chat from your own button

If your site already has a "Live support" button, you can hide the launcher entirely. Pick launcher behaviour: on request (launcher_mode: manual) in the panel and wire one line to your button:

html
<button onclick="Signalbird.chat.open()">Live support</button>

Once a conversation starts the launcher becomes visible: if the visitor closes the window, the agent's reply is announced there with a badge and a sound. When the visitor ends the chat the launcher disappears again, and the agent can no longer write to that conversation.

Identity verification and captcha

Identity verification (2.7.0). The public key sits in your page source, so anyone can call identify({ external_id: 'user-1042' }). Signalbird therefore treats external_id (and email) as trusted only when a valid identityHash comes with it. Without the hash:

  • the visitor is not linked to your contact record,
  • the device is not bound to that user as a push target (external:user-1042),
  • the channel agent's tools do not get it as a trusted visitor identity, only under visitor.unverified.

The hash is generated on YOUR server with the secret key and written into the page next to the user's id; the secret key never reaches the browser:

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

Every server language ships a helper: PHP Signalbird::identityHash($id), Node client.identityHash(id), Python client.identity_hash(id), Go client.IdentityHash(id), .NET client.IdentityHash(id).

html
<!-- Laravel 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>
javascript
Signalbird.identify({ external_id: 'user-1042', identityHash: hashFromYourServer, email: 'jane@example.com' })

The widget adds the known hash to the session, identify and (same external_id) push.register calls on its own.

Captcha. When enabled on your channel, the widget obtains an invisible Cloudflare Turnstile token at two points: when a new visitor's session is created and when an unverified visitor starts their first conversation. Nothing to set up:

  • The Turnstile script loads only when the visitor first opens the chat; visitors who never open it never download it.
  • Most visitors see nothing; if Cloudflare needs an interaction, the box shows up inside the panel.
  • If too many chats are started in a short time the visitor sees a polite notice (429 CONVERSATION_RATE_LIMITED).
  • If you use a Content Security Policy, add https://challenges.cloudflare.com to script-src and frame-src.

Privacy and security

  • The visitor id and secret live only in localStorage['sb_visitor']; no cookies are written, nothing goes to third parties.
  • Live translation runs only when the two sides use different languages, and each side sees only the OTHER side's message translated; your own text always stays as you wrote it.
  • Internal notes your team writes in Inbox are never sent to the visitor.
  • The app key is public; protection is the allowed-origins list. Keep it tight (no *).
  • The widget does not read your page's DOM or touch your form fields; it only works inside its own Shadow DOM root.
  • Uploaded files go to your team's storage and are visible only to the parties of that conversation.

Troubleshooting

Symptom Cause / fix
Widget never appears, console is quiet Chat is disabled on the app (chat_enabled: false). Enable it in the panel
bootstrap 403 in the console Domain is not in allowed origins - add it with the https:// scheme; localhost:3000 must be added separately
bootstrap 401 Wrong key type (secret instead of public) or the key was revoked
Bubble shows, messages don't send Check /v1/sdk/* requests in the Network tab; debug: true logs details to the console
No history after login identify was not called, or a different external_id was used
Visitor not linked to the contact, push doesn't reach the user identityHash missing or generated with the wrong key - identity is treated as unverified
"We couldn't complete the security check" banner Turnstile failed to load or was rejected; check that your CSP allows challenges.cloudflare.com
Site CSS broke the widget Should not happen (Shadow DOM). If the page has aggressive global rules like all: unset, report it with debug output