Set up web push
Generate your workspace's web push keys, add the Customer.io service worker and web push plugin to your website, and ask visitors for permission to send them notifications.
Before you begin
- Add our JavaScript source to your website and connect it to the workspace you’ll send web push from. The web push plugin works with the source you already have; you don’t add a second snippet.
- Serve your website over HTTPS. Browsers only allow web push on secure sites. You can use
http://localhostwhile you develop. - Identify people when they sign in. Customer.io adds a browser to a person’s profile only after you identify them, so you can only offer notifications to signed-in visitors.
- Choose one website. During the alpha, web push supports one website per workspace. A web push action can’t target a website yet, so if you set up web push on a second website, people who subscribed on either site get every notification. Support for more than one website is coming.
1. Generate your web push keys
Your workspace has one key pair for web push. Browsers use the public key to subscribe, and Customer.io uses the private key to sign each notification. You don’t need an account with Apple, Google, or another push provider.
- Go to Settings > Push > Web.
- In VAPID subject, enter a contact address for your company, like
mailto:push@yourbrand.comorhttps://yourbrand.com/contact, using your own public domain. Push services use it to contact you if there’s a problem with your notifications. - Click Generate keys, then click Save.
- Click Copy public key. You’ll add it to your website in step 3.
The subject must use a real, public domain that you control. Apple’s push service rejects notifications for subjects on domains like .test, .local, or localhost, so Safari subscribers wouldn’t receive anything. The subject is your company’s contact address; it isn’t your website’s address or a subscriber’s email, so use a public address even when you test on localhost.
Your public key isn’t a secret, so you can put it in your website’s code. Customer.io never shows your private key.
2. Add the service worker to your website
A service worker is a JavaScript file that the browser runs in the background. The Customer.io service worker shows each notification, opens your link when someone clicks it, and reports delivered and opened metrics. Browsers only run a service worker that comes from your own domain, so you need to host a copy of the file on your website.
- Download
cio-webpush-sw.jsfromhttps://cdp.customer.io/v1/analytics-js/cio-webpush-sw.js. If your account is in our EU region, usehttps://cdp-eu.customer.io/v1/analytics-js/cio-webpush-sw.js. If you install the SDK from npm, the same file is innode_modules/@customerio/cdp-analytics-browser/dist/cio-webpush-sw.js. - Put the file in your website’s public root, so that it loads from
https://your-site.com/cio-webpush-sw.js. - Check that the URL returns the file as JavaScript over HTTPS, without a redirect.
Your copy of cio-webpush-sw.js never updates itself, even if you load the SDK from our CDN snippet. Whenever release notes say the service worker changed, download the file again and replace your copy.
If your site already has a service worker
Don’t replace your existing service worker. A page can only have one service worker for a given path, and the plugin won’t replace a different worker at the same path. Choose one of these approaches:
-
Import our worker into yours. Add these lines to the top of your existing service worker, and set
serviceWorkerUrlto your worker’s path in step 3. SettingcioWebPushLifecycletofalsekeeps your worker in charge of its own updates. Our handlers ignore notifications that didn’t come from Customer.io, so they work alongside other push code.JAVASCRIPTself.cioWebPushLifecycle = false; importScripts('/cio-webpush-sw.js');The plugin registers your worker with a
?track=query that tells our code where to report metrics. If your own code also registers the worker, keep that query in the URL, or you’ll lose delivered and opened metrics. If you import our file this way, your worker must keep callingskipWaiting()andclients.claim()itself, because our code skips them when imported.importScriptsonly works in a classic service worker. If your site uses a module service worker, leave it alone and serve our worker in its own scope instead, for example at/notifications/cio-webpush-sw.js; the plugin always registers its worker as a classic script, so bundling our code into a module worker doesn’t work. -
Give our worker its own path. Put the file in a folder, like
/notifications/cio-webpush-sw.js, and setserviceWorkerUrlto that path. Your existing worker keeps the rest of your site.
3. Add the web push plugin
The web push plugin adds a webPush object to your JavaScript source. Loading the plugin doesn’t ask for permission or register the service worker; that only happens when a visitor clicks your button in step 4.
Snippet
Add this after your existing JavaScript source snippet. Replace YOUR_PUBLIC_KEY with the key you copied in step 1. If your account is in our EU region, load the plugin from cdp-eu.customer.io and set trackUrl to https://track-eu.customer.io.
<script src="https://cdp.customer.io/v1/analytics-js/webPushPlugin.js"></script>
<script>
cioanalytics.ready(async () => {
// The plugin script was blocked or failed to load: leave your button disabled.
if (!window.CustomerIOWebPush) {
console.warn('Customer.io web push did not load, so notifications are unavailable on this page.');
return;
}
await cioanalytics.register(CustomerIOWebPush.WebPushPlugin({
vapidPublicKey: 'YOUR_PUBLIC_KEY',
serviceWorkerUrl: '/cio-webpush-sw.js',
trackUrl: 'https://track.customer.io'
}));
});
</script>Ad blockers, a Content Security Policy, or a network error can stop webPushPlugin.js from loading. The check for window.CustomerIOWebPush keeps your page from throwing an error when that happens; tracking still works, and your button stays disabled.
npm
Register the plugin after you load the SDK. Replace YOUR_PUBLIC_KEY with the key you copied in step 1. If your account is in our EU region, set cdnURL to https://cdp-eu.customer.io when you load the SDK, and set trackUrl to https://track-eu.customer.io.
import { AnalyticsBrowser, WebPushPlugin } from '@customerio/cdp-analytics-browser'
const [analytics] = await AnalyticsBrowser.load({
writeKey: 'YOUR_WRITE_KEY',
// cdnURL: 'https://cdp-eu.customer.io', // Set if you're in our EU region
})
await analytics.register(WebPushPlugin({
vapidPublicKey: 'YOUR_PUBLIC_KEY',
serviceWorkerUrl: '/cio-webpush-sw.js',
trackUrl: 'https://track.customer.io',
}))| Option | Description |
|---|---|
vapidPublicKey | The public key from Settings > Push > Web. |
serviceWorkerUrl | The path to the service worker on your site. Defaults to /cio-webpush-sw.. It must be on the same domain as your page. |
trackUrl | Where the service worker reports delivered and opened metrics. Defaults to https://track.. Use https://track-eu. in the EU region. |
If your site uses a Content Security Policy, allow the plugin’s script host (cdp.customer.io or cdp-eu.customer.io) and the track host (track.customer.io or track-eu.customer.io). See Content Security Policy.
4. Ask for permission in a click
Browsers only show the permission prompt when someone clicks or taps something on your page. Add a button that explains what people get, like Turn on booking alerts, and call webPush.subscribe() when they click it.
subscribe() asks for permission, subscribes the browser, and adds a web device to the identified person’s profile. Identify the person when they sign in, before they can click the button.
<button id="enable-alerts" disabled>Turn on booking alerts</button>
<button id="disable-alerts" disabled>Turn off alerts</button>
<p id="alert-status" aria-live="polite"></p>
<script>
cioanalytics.ready(async () => {
const enable = document.querySelector('#enable-alerts');
const disable = document.querySelector('#disable-alerts');
const status = document.querySelector('#alert-status');
// Your plugin registration from step 3 goes here.
// Identify the signed-in person before you offer alerts.
await cioanalytics.identify(currentUser.id, { email: currentUser.email });
async function refresh() {
const on = Boolean(await cioanalytics.webPush.subscription());
enable.disabled = on;
disable.disabled = !on;
status.textContent = on ? 'Booking alerts are on.' : 'Booking alerts are off.';
}
enable.onclick = () => {
enable.disabled = true;
// Call subscribe() first, with no await before it, so Safari shows the prompt.
cioanalytics.webPush.subscribe().then(refresh).catch((error) => {
enable.disabled = false;
status.textContent = Notification.permission === 'denied'
? 'Notifications are blocked. Allow them in your browser settings, then try again.'
: error.message;
});
};
disable.onclick = () => cioanalytics.webPush.unsubscribe().then(refresh).catch((error) => {
status.textContent = error.message;
});
await refresh();
});
</script>Replace currentUser with however your site stores the signed-in person.
A few more things to build into your page:
- Hide the button in browsers that can’t receive web push. Check for
serviceWorkerinnavigator,PushManagerinwindow, andNotificationinwindowbefore you show it. - Show recovery steps when notifications are blocked. When
Notification.permissionisdenied, the browser won’t ask again, and your page can’t change that. Tell people how to allow notifications in their browser, then ask them to click your button again. See troubleshooting. - Call
cioanalytics.reset()when someone signs out. It removes the browser from that person’s profile, so the next person who signs in on the same browser doesn’t get their notifications. Identify the next person before you offer alerts again. - Let people turn alerts off.
webPush.unsubscribe()unsubscribes the browser and removes the device from the person’s profile.
If someone subscribes before you identify them, the browser subscribes, but Customer.io doesn’t add the device until you call identify.
Device attributes
When the plugin adds a web device, it sets the device’s origin attribute to the website the browser subscribed on (the page’s location.origin, like https://www.yourbrand.com). Browsers that subscribed with an earlier version of the plugin get the attribute on their next identified visit to your site. Customer.io doesn’t use origin to choose who gets a notification yet; it records the website so that you can target websites when support for more than one website per workspace arrives. If you load the plugin with the snippet, it updates automatically from our CDN and records origin without any change on your side. If you install the SDK from npm, use version 0.7.0 or later to record origin.
Plugin methods
| Method | Returns | Description |
|---|---|---|
webPush. | Promise | Asks for permission if needed, subscribes the browser, and adds a web device to the identified person. Rejects if the browser doesn’t support web push or the person doesn’t allow notifications. |
webPush. | Promise | The browser’s current subscription, or null if it isn’t subscribed. |
webPush. | Promise | Unsubscribes the browser and removes the web device from the person’s profile. |
webPush. | Promise | Asks for permission without subscribing. You don’t need it, because subscribe() asks for permission. Don’t call it before subscribe() in Safari. |
iOS and iPadOS
On iPhone and iPad, web push only works in a web app that the person adds to their Home Screen. A normal Safari tab doesn’t have the notification APIs, so your button can’t work there.
To support iOS and iPadOS 16.4 and later:
-
Add a web app manifest to your site, with
displayset tostandaloneorfullscreen. Serve it from your HTTPS domain, for example as/manifest.webmanifest:JSON{ "id": "/", "name": "Your App", "short_name": "Your App", "start_url": "/", "scope": "/", "display": "standalone", "icons": [ { "src": "/icons/app-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/app-512.png", "sizes": "512x512", "type": "image/png" } ] } -
Link the manifest and a Home Screen icon in the
<head>of your pages:HTML<link rel="manifest" href="/manifest.webmanifest"> <link rel="apple-touch-icon" href="/icons/apple-touch-icon-180.png"> -
In a normal Safari tab, show instructions instead of your button: tap Share, then Add to Home Screen, then open your app from the Home Screen and sign in. You can tell that your page is running as a Home Screen app when
window.matchMedia('(display-mode: standalone)').matchesornavigator.standaloneistrue. -
In the Home Screen app, people tap your button to subscribe, just like on desktop.
Notifications on iOS and iPadOS use your Home Screen app’s icon and name.
5. Test your setup
A successful subscription doesn’t prove that notifications arrive. Test the whole path with your own browser before you send to your audience.
- Sign in to your site as a test person and click your button. Allow notifications when the browser asks.
- In Customer.io, open the test person’s profile and go to Devices. You should see a web device; its token is the browser’s push address, which starts with
https://. Expand the device to see Site, the website the browser subscribed on, likehttps://www.yourbrand.com. The API stores this asattributes.origin. - Send a test from Settings > Push > Web or the message’s Send test… button. Both ask for the subscription JSON: run
JSON.stringify(await cioanalytics.webPush.subscription())in your browser’s JavaScript console on your site, and paste the result. - Check that the notification appears, and that clicking it opens your link. Settings and composer test sends check the push relay without creating a real automation delivery. They don’t create an automation Sent tab entry or automation delivery metrics.
- To verify delivery history and metrics, create a test automation with a Web Push action and set its Sending behavior to Send Automatically. Queue Draft holds messages for review until you send them manually. Trigger the automation for your test profile. Use a trigger you control, like a test event, and keep the audience limited to your test profile.
- Open the automation’s Sent tab and find that real delivery. Check Sent, then Delivered after the notification appears, and Opened after you click it. These metrics belong to the automation delivery, separate from the earlier test send.
If nothing arrives, see troubleshooting.