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.

Web push is turned on per account

Web push is available to accounts that Customer.io has turned it on for. If you don’t see a Web tab under Settings > Push, contact your account manager.

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://localhost while 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.

  1. Go to Settings > Push > Web.
  2. In VAPID subject, enter a contact address for your company, like mailto:push@yourbrand.com or https://yourbrand.com/contact, using your own public domain. Push services use it to contact you if there’s a problem with your notifications.
  3. Click Generate keys, then click Save.
  4. 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.

Don't regenerate your keys after you go live

Each browser subscription only works with the public key it subscribed with. If you click Regenerate, every existing subscriber stops receiving web push until they visit your site and subscribe again. To change your contact address, edit VAPID subject and click Save; you don’t need new keys.

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.

  1. Download cio-webpush-sw.js from https://cdp.customer.io/v1/analytics-js/cio-webpush-sw.js. If your account is in our EU region, use https://cdp-eu.customer.io/v1/analytics-js/cio-webpush-sw.js. If you install the SDK from npm, the same file is in node_modules/@customerio/cdp-analytics-browser/dist/cio-webpush-sw.js.
  2. Put the file in your website’s public root, so that it loads from https://your-site.com/cio-webpush-sw.js.
  3. Check that the URL returns the file as JavaScript over HTTPS, without a redirect.

When you update the SDK, copy the new version of the file to your website.

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 serviceWorkerUrl to your worker’s path in step 3. Setting cioWebPushLifecycle to false keeps 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.

    JAVASCRIPT
    self.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 calling skipWaiting() and clients.claim() itself, because our code skips them when imported. importScripts only 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 set serviceWorkerUrl to 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.

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.

HTML
<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.

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.

JAVASCRIPT
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',
}))
OptionDescription
vapidPublicKeyThe public key from Settings > Push > Web.
serviceWorkerUrlThe path to the service worker on your site. Defaults to /cio-webpush-sw.js. It must be on the same domain as your page.
trackUrlWhere the service worker reports delivered and opened metrics. Defaults to https://track.customer.io. Use https://track-eu.customer.io 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.

HTML
<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.

Safari: call subscribe() directly in your click handler

Safari only shows the permission prompt if your code calls subscribe() right away when someone clicks. Don’t wait for anything first, like identify, a network request, a timer, or your own Notification.requestPermission() call. If you do, Safari refuses to subscribe, even after the person clicks Allow. It’s fine to handle the result of subscribe() afterward.

A few more things to build into your page:

  • Hide the button in browsers that can’t receive web push. Check for serviceWorker in navigator, PushManager in window, and Notification in window before you show it.
  • Show recovery steps when notifications are blocked. When Notification.permission is denied, 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 the next time the person visits 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 install the SDK from npm, use version 0.7.0 or later to record origin.

Plugin methods

MethodReturnsDescription
webPush.subscribe()PromiseAsks 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.subscription()PromiseThe browser’s current subscription, or null if it isn’t subscribed.
webPush.unsubscribe()PromiseUnsubscribes the browser and removes the web device from the person’s profile.
webPush.requestPermission()PromiseAsks 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:

  1. Add a web app manifest to your site, with display set to standalone or fullscreen. 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" }
      ]
    }
  2. 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">
  3. 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)').matches or navigator.standalone is true.

  4. 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.

  1. Sign in to your site as a test person and click your button. Allow notifications when the browser asks.
  2. 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, like https://www.yourbrand.com. The API stores this as attributes.origin.
  3. 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.
  4. 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.
  5. To verify delivery history and metrics, create a test automation with a Web Push action and trigger it for your test profile. Use a trigger you control, like a test event, and keep the audience limited to your test profile.
  6. 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.

Updated October 22, 2026