ReachBellDocs

Browser SDK reference

This is the full method reference for @reachbell/sdk, the browser SDK. Every framework wrapper — @reachbell/react, @reachbell/vue, @reachbell/nextjs — drives the same core, so the behaviour documented here holds everywhere; only the ergonomics differ.

If you're just getting set up, start with SDK installation or the JavaScript integration walkthrough. This page is the API surface you reach for once it's running.

The SDK is exposed two ways, and they're the same instance:

import { ReachBell } from "@reachbell/sdk"; // npm
// or, via the CDN <script>, the global:
window.ReachBell;

The SDK doesn't throw for expected failures. Network-touching methods resolve with a structured result instead of throwing — prompt() → { success, token?, reason? }, and identify() / tag() → { ok, reason? }. Inspect the result and log the reason; don't wrap every call in try/catch.

init

ReachBell.init(config: ReachBellConfig): void

Configures the SDK, fetches your project context (GET /projects/context), and registers the service worker. Call it once per page load. It returns void and bootstraps in the background — await ReachBell.ready if you need to know the bootstrap finished.

init() is idempotent: a second call is a no-op and logs a warning. Pass force: true to replace the config and re-run the bootstrap.

Config options

interface ReachBellConfig {
  apiKey: string;        // required — your project's public key (rb_live_…)
  apiUrl?: string;       // default "https://api.reachbell.com"
  swPath?: string;       // default "/reachbell-sw.js"
  autoPrompt?: boolean;  // default false
  debug?: boolean;       // default false — logs to console with a [ReachBell] prefix
  force?: boolean;       // default false — re-initialize, replacing config
}
  • apiKey — required. Identifies your project. This is the public/client key, safe to ship in browser code; it is not the server API key.
  • apiUrl — override only on a self-hosted or regional deployment.
  • swPath — the path the service worker is registered from. The default /reachbell-sw.js is the root, which is what you want — a service worker's scope is bound to where it's served from. Change this only if you genuinely serve the file elsewhere (and accept the narrower scope).
  • autoPrompt — when true, the SDK arms a one-shot click / keydown / touchstart listener and fires the prompt on the user's first interaction. Chromium browsers refuse to show the native permission dialog without a user gesture, so this is the safe way to "auto" prompt.
ReachBell.init({ apiKey: "rb_live_yourkey", autoPrompt: true });

ready

ReachBell.ready: Promise<void>

A getter that resolves once the boot chain (context fetch + service-worker registration) has settled. Rejects if you read it before calling init().

ReachBell.init({ apiKey: "rb_live_yourkey" });
await ReachBell.ready;
// context + service worker are now in place

You rarely need this — prompt() already awaits the in-flight bootstrap internally — but it's there when you want to gate UI on a ready SDK.

prompt

ReachBell.prompt(): Promise<SubscribeResult>

Shows the opt-in prompt configured in the dashboard, then escalates to the browser's native permission dialog if the user accepts, and finally subscribes the device. This is the one call that takes a user from "never asked" to "subscribed".

interface SubscribeResult {
  success: boolean;
  token?: string;   // present on success
  reason?: string;  // present on failure
}
const result = await ReachBell.prompt();
if (result.success) {
  console.log("Subscribed. Token:", result.token);
} else {
  console.warn("Prompt declined or failed:", result.reason);
}

If the dashboard prompt is a soft prompt (type soft_prompt or bell_widget), the SDK shows that first and only escalates to the native dialog when the user accepts it. A native-type prompt (or no prompt configured) goes straight to the browser dialog.

Common reason values on failure:

reasonWhat happened
sdk_not_initializedprompt() was called before init().
push_not_supportedThe browser has no PushManager (e.g. iOS outside an installed PWA).
user_declined_soft_promptThe user dismissed the dashboard soft prompt before the native dialog.
permission_deniedThe user blocked the native permission dialog.
sw_register_failed_*The service worker failed to register (suffix is the DOM error name).
push_subscribe_failed_*pushManager.subscribe() rejected (suffix is the DOM error name, e.g. NotAllowedError).
subscribe_failed_<status>The API rejected the subscription (suffix is the HTTP status).
not_readyThe service worker or server context wasn't available.
timed_out / network_errorThe subscribe request to the API timed out or the network failed.

You can read the permission state directly off the Notification API before calling prompt():

if (Notification.permission === "granted") {
  // already subscribed — no need to prompt
} else if (Notification.permission === "denied") {
  // blocked — the native dialog will not re-prompt
} else {
  await ReachBell.prompt();
}

No subscribe() / unsubscribe() methods. Subscribing is what prompt() does (it owns the soft-prompt → permission → subscribe flow). There is no separate unsubscribe() call in the browser SDK — a user revokes push through their browser's site settings, and you remove the record server-side with subscribers.delete or DELETE /subscribers/{token}. The current subscription token isn't read back through a getSubscription() accessor either; it's returned from prompt() on success and held internally for identify() / tag().

identify

ReachBell.identify(externalId: string): Promise<ApiResult>

Links the current device subscription to your internal user ID, so you can target sends by user instead of by an opaque token. Call it after the user logs in.

interface ApiResult {
  ok: boolean;
  reason?: string;
}
const result = await ReachBell.identify("user_42");
if (!result.ok) console.warn("identify failed:", result.reason);

identify() is idempotent. The same externalId across multiple devices links them all to one logical subscriber in the dashboard. If the device isn't subscribed yet, it resolves { ok: false, reason: "no_subscription" } — call prompt() first. Other failure reasons: sdk_not_initialized, server_<status>, timed_out, network_error.

tag

ReachBell.tag(tags: string[]): Promise<ApiResult>

Attaches flat string labels to the device for segmentation. Tags are additive — each call adds these labels to the device's existing set rather than replacing it.

await ReachBell.tag(["premium", "beta-tester"]);

Returns the same { ok, reason } shape as identify(), with the same failure reasons (including no_subscription if the device isn't subscribed). The dashboard's segment builder reads these tags directly.

There is no untag() method in the browser SDK — to remove a tag, call POST /subscribers/untag from your backend (see the Subscribers API).

track

Event tracking lives on the server side, not in the browser SDK. There is no ReachBell.track() in @reachbell/sdk — send events from your backend with the Server SDK's events.track() / events.trackBatch(), or POST /events directly (see the Events API). Keeping event ingestion server-side means events can't be spoofed from a user's browser.

In-page widgets

The same package can render UI inside your page DOM — no notification permission required, because nothing leaves the browser. These are useful for real-time alerts to users who are already on your site. They are not push notifications and they don't send anything; to deliver a message in or out of the page, compose a campaign in the dashboard.

All three are rendered imperatively (via element.style.*, not inline style="…" attributes), so they work on sites with a strict Content-Security-Policy: style-src 'self' without needing 'unsafe-inline'. Any colour you pass is validated before use — a malformed value falls back to the ReachBell default rather than being applied.

showToast

ReachBell.showToast(opts: ToastOptions): () => void

A stacking corner toast. Returns a dismiss() function; toasts also auto-dismiss after duration ms. Multiple toasts stack at the chosen corner.

interface ToastOptions {
  title: string;          // required — bold title line
  body?: string;          // subtitle / body line
  url?: string;           // navigates the page when the card is clicked
  duration?: number;      // auto-dismiss in ms. Default 5000. 0 = no auto-dismiss
  position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; // default "bottom-right"
  accentColor?: string;   // accent-dot colour. Default ReachBell blue
}
const dismiss = ReachBell.showToast({
  title: "Order shipped 📦",
  body: "Order #4821 arrives tomorrow.",
  url: "/orders/4821",
  duration: 6000,
});
// dismiss() to remove it early

showBanner

ReachBell.showBanner(opts: BannerOptions): () => void

A sticky banner across the top or bottom of the viewport. Returns dismiss(). There's one banner per position — a second call at the same position replaces the first.

interface BannerOptions {
  title: string;            // required — headline
  body?: string;            // optional subtitle
  url?: string;             // navigates when the banner is clicked
  position?: "top" | "bottom"; // default "top"
  duration?: number;        // auto-dismiss in ms. Default 0 (sticky)
  backgroundColor?: string; // default ReachBell blue
  textColor?: string;       // default white
  dismissible?: boolean;    // show the × close button. Default true
  ctaText?: string;         // call-to-action label shown next to the body
}
ReachBell.showBanner({
  title: "🚀 Black Friday — 30% off everything",
  body: "Code BLACK30 — ends midnight",
  ctaText: "Shop now",
  url: "/sale",
});

showNotificationCenter

ReachBell.showNotificationCenter(opts?: NotificationCenterOptions): () => void

Renders a floating bell-icon button anchored in a corner; clicking it opens a dropdown of recent notifications with an unread badge. Calling it twice replaces the existing widget so you can re-skin without reloading. The returned function tears the widget down.

interface NotificationCenterOptions {
  position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; // default "bottom-right"
  maxItems?: number;     // how many recent items to show. Default 20
  accentColor?: string;  // override the accent colour
  title?: string;        // dropdown heading. Default "Notifications"
  emptyText?: string;    // shown when there are no items
}
ReachBell.showNotificationCenter({ position: "bottom-right" });

The dropdown reads from a local history store. You populate it with recordNotification() — typically from your service worker (or anywhere you receive a push payload):

ReachBell.recordNotification({
  title: "Order shipped 📦",
  body: "Order #4821 arrives tomorrow.",
  url: "/orders/4821",
});

Companion helpers for that store:

  • getNotificationHistory(limit = 20) → returns the stored items, newest first.
  • clearNotificationHistory() → empties the store.

recordNotification() is safe to call before init(), dispatches a reachbell:notification-recorded window event (so an open center refreshes live), and keeps a bounded history with the oldest entries evicted first.

attachUtmTagger

ReachBell.attachUtmTagger(utm: {
  source: string;
  medium: string;
  campaign?: string;
  term?: string;
  content?: string;
}): void

Installs a single delegated click listener that appends UTM parameters to outbound (cross-origin) link clicks, so your analytics attributes the session to ReachBell. Safe to call repeatedly — it installs the listener only once.

ReachBell.attachUtmTagger({
  source: "reachbell",
  medium: "push",
  campaign: "fall-2026",
});

Same-origin links are left untouched, and any UTM param already present on a URL wins. See the UTM tracking guide for conventions.

What's next?