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? }, andidentify()/tag()→{ ok, reason? }. Inspect the result and log thereason; don't wrap every call intry/catch.
init
ReachBell.init(config: ReachBellConfig): voidConfigures 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.jsis 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— whentrue, the SDK arms a one-shotclick/keydown/touchstartlistener 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 placeYou 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:
reason | What happened |
|---|---|
sdk_not_initialized | prompt() was called before init(). |
push_not_supported | The browser has no PushManager (e.g. iOS outside an installed PWA). |
user_declined_soft_prompt | The user dismissed the dashboard soft prompt before the native dialog. |
permission_denied | The 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_ready | The service worker or server context wasn't available. |
timed_out / network_error | The 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 whatprompt()does (it owns the soft-prompt → permission → subscribe flow). There is no separateunsubscribe()call in the browser SDK — a user revokes push through their browser's site settings, and you remove the record server-side withsubscribers.deleteorDELETE /subscribers/{token}. The current subscription token isn't read back through agetSubscription()accessor either; it's returned fromprompt()on success and held internally foridentify()/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): () => voidA 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 earlyshowBanner
ReachBell.showBanner(opts: BannerOptions): () => voidA 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): () => voidRenders 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;
}): voidInstalls 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?
- New to the SDK? Walk through the JavaScript integration end to end.
- Using a framework? See the React, Vue, or Next.js guides for the hook/composable equivalents.
- Sending and tracking from a backend? That's the Server SDK and the Server API.