Skip to main content
engage

Web push

active
Audience: developerUpdated 2026-07-27

This how-to makes a browser reachable on Engage's web push channel. Three things happen: your site gets a push token from Firebase, you hand that token to the Web SDK, and Binoban's service worker displays and tracks whatever arrives.

Before you start

  • The Web SDK is loaded on your site and tracking.
  • Your Firebase project is set up per Set up Firebase for Binoban push, and you have your VAPID public key.
  • Your site is served over HTTPS (or localhost in development) — the Push API is unavailable otherwise.
  • You have the binoban-messaging.js URL from Binoban (see Step 1).

Step 1 — Load the Binoban service worker

binoban-messaging.js is deployed by Binoban to the infrastructure you provide, and Binoban gives you its URL — the same arrangement as the Web SDK bundle itself. You do not build, host, or update this file. If you do not have the URL, ask your Binoban contact.

What is yours is the service worker the browser registers. A service worker must be served from your own origin and can only control pages within its scope, so keep a worker file at your site root and pull Binoban's logic into it:

// /firebase-messaging-sw.js  — served from your own origin
importScripts('YOUR_BINOBAN_MESSAGING_URL')

That single line is the whole worker. importScripts is allowed to load cross-origin, so Binoban's URL may live on a different host than your site — only your worker file is bound by the same-origin rule.

You do not need Firebase inside the worker

Binoban's worker listens for the browser's raw push event and parses the message itself, so it works without the Firebase messaging library loaded in the service worker. If you already run Firebase there for your own messages, keep it — the two coexist, provided you add the guard below.

Tell Binoban's pushes apart from your own

Every message Binoban sends carries source: "binoban" in its data payload. The backend sets it; you never set it yourself. It is present on every platform — web, Android, and iOS alike — and it is the marker that keeps Binoban's push handling and your own from colliding.

Binoban's side is already handled: its worker drops any payload whose source is not "binoban", so your messages never reach it. Your side is the half you write. Because Binoban's worker displays the notification itself, any handler of yours that also displays one will double-display unless it skips Binoban's messages:

// /firebase-messaging-sw.js
importScripts('YOUR_BINOBAN_MESSAGING_URL')

self.addEventListener('push', (event) => {
let payload
try {
payload = event.data ? JSON.parse(event.data.text()) : undefined
} catch {
return
}

// Binoban's own listener already displayed and tracked this one.
if (payload?.data?.source === 'binoban') return

// ...your own notification handling
})

Apply the same guard wherever else you handle incoming pushes:

  • Firebase's onBackgroundMessage in the worker — check payload.data?.source.
  • Firebase's onMessage on the page. A push event reaches the service worker whether or not your tab is focused, so Binoban has already displayed the notification by the time onMessage runs. An unguarded handler that shows its own notification double-displays foreground pushes.

Step 2 — Ask permission and get a token

Request notification permission, register the worker, and ask Firebase for a token bound to that registration:

import { initializeApp } from 'firebase/app'
import { getMessaging, getToken } from 'firebase/messaging'

const app = initializeApp(firebaseConfig)
const messaging = getMessaging(app)

async function subscribeToPush() {
const permission = await Notification.requestPermission()
if (permission !== 'granted') return

const registration = await navigator.serviceWorker.register(
'/firebase-messaging-sw.js'
)

const token = await getToken(messaging, {
vapidKey: 'YOUR_VAPID_PUBLIC_KEY',
serviceWorkerRegistration: registration,
})

if (token) registerWithBinoban(token)
}

Call this from a user gesture — a "Turn on notifications" button — rather than on page load. Browsers penalise sites that prompt unprompted, and a denied permission cannot be re-requested.

Step 3 — Hand the token to Binoban

One track call registers the browser:

function registerWithBinoban(token) {
Binoban.track('notification_registered', { key: token })
}

Call it again whenever Firebase issues a refreshed token, so Engage always holds the current one.

The Web SDK rewrites this event to bb_notification_registered on the way out and stamps sdk: "WEB" and the current device_id onto it — that renamed event is what tells Engage the browser is reachable. See Push events.

Step 4 — What the worker does from here

Once a browser is subscribed, everything below is handled for you. There is nothing further to write:

  • Displays the notification with the title, body, icon, image, and badge from the payload, honouring requireInteraction.
  • Reports delivery the moment it displays — and reports failed instead if the browser refuses to show it.
  • Renders up to two action buttons. Browsers that do not support notification actions (Safari, for example) simply ignore them.
  • Reports the click, including which button was pressed and the resolved target URL.
  • Opens the target — focusing an already-open tab for that URL if there is one, otherwise opening a new window. A payload with no target falls back to your site.
  • Reports dismissals when the user closes the notification.

Step 5 — Verify

  1. In DevTools → Application → Service Workers, your worker is activated and running.
  2. In DevTools → Application → Notifications, permission is granted.
  3. Your next flushed batch contains a bb_notification_registered event carrying the token.
  4. Send a test campaign from the panel; the notification appears, and bb_notification_delivered follows.

Known limits

  • There is no subscribe() helper in the Web SDK. Requesting permission and registering the worker stay yours by design — they are ordinary browser APIs, and when to ask for consent is a decision about your site's UX, not something an SDK should make.
  • customData is not surfaced on the web. The worker does not read it; use the target URL to carry context instead. It is available on the native SDKs.
  • Two action buttons maximum on the web, against three on native.

Next steps