Skip to content

Notifications · API

Web Push and PushManager.subscribe()

Published

Limited availabilityNot supported in Safari (iOS)W3C

registration.pushManager.subscribe() asks the browser’s push service for a PushSubscription whose endpoint your server can POST encrypted messages to; the browser wakes the service worker with a push event for each message even when no page is open, and the worker shows a notification. Together with the Notifications API this is Web Push, the only standard way for a server to reach a PWA user who has closed the app.

Sequence diagram of Web Push across the web page, service worker, push service and application server: the page requests notification permission, calls pushManager.subscribe() with the VAPID public key, receives a PushSubscription and posts it to the server. The server sends a VAPID-signed, encrypted request to the endpoint, the push service delivers it, the browser fires the push event in the service worker, which calls showNotification(). A tap fires notificationclick, and pushsubscriptionchange triggers a resubscribe.
Web Push from subscription to notification: who talks to whom, and which event fires where.
registration.pushManager.subscribe(options)
registration.pushManager.getSubscription()
registration.pushManager.permissionState(options)
subscription.toJSON()
subscription.unsubscribe()

subscribe() resolves with a PushSubscription, or with the existing one when the origin is already subscribed with the same key. getSubscription() resolves with the current subscription or null. All three are available from a window and from the service worker’s own self.registration. Support: Chrome 42, Firefox 44, Edge 17, Safari 16 on macOS Ventura, Safari 16.4 on iOS for Home Screen web apps (BCD api.PushManager.subscribe). Firefox 72+ requires subscribe() to run inside a user gesture; Chromium requires applicationServerKey.

Parameter Type Meaning
options.userVisibleOnly boolean Promise that every push results in a visible notification. Chromium and WebKit reject false with NotAllowedError; WebKit also revokes a subscription whose push handler fails to show one (Meet Web Push, webkit.org). Firefox accepts false and applies a quota to silent pushes instead.
options.applicationServerKey BufferSource or base64url string Your VAPID public key, an uncompressed P-256 point of 65 bytes. The server signs each send with the matching private key (RFC 8292, ietf.org). Required in Chromium; a subscription without it in Firefox cannot be migrated to VAPID later without unsubscribing.

The returned PushSubscription serialises with toJSON() to { endpoint, expirationTime, keys: { p256dh, auth } }. endpoint is a capability URL on the browser vendor’s push service (https://fcm.googleapis.com/fcm/send/… for Chrome, a *.push.apple.com host for Safari); anyone holding it can send to the device, so it is transmitted and stored like a credential. p256dh and auth are the client’s public key and authentication secret for RFC 8291 payload encryption. expirationTime is null in every current engine.

Exception When
NotAllowedError DOMException Notification permission is "denied", or the user dismissed the prompt that subscribe() triggered; userVisibleOnly is false in Chromium or WebKit; the call is outside a user gesture in Firefox 72+; the service worker’s scope is not a secure context.
InvalidStateError DOMException The registration has no active worker, or a subscription already exists with a different applicationServerKey. Unsubscribe before changing keys.
InvalidAccessError DOMException applicationServerKey is not a valid P-256 public key (wrong length or not an uncompressed point).
InvalidCharacterError DOMException The base64url string form of applicationServerKey fails to decode.
AbortError DOMException The push service could not create the subscription, typically because the device is offline or the service is unreachable. Retry later.

Server-side failures are HTTP statuses from the push service, not exceptions: 404 and 410 mean the subscription is gone and must be deleted; 413 means the payload exceeds the 4 KB limit; 429 means the service is rate-limiting the application server.

The page subscribes, the service worker receives, and a third snippet checks what already exists before doing either.

Subscribing from a click and posting the subscription to the server

Section titled “Subscribing from a click and posting the subscription to the server”

Permission and subscription happen in one gesture. The key arrives from the server as a base64url string, which the specification allows subscribe() to take directly; the urlBase64ToUint8Array() conversion seen in older tutorials is only needed for engines that predate that change.

button.addEventListener('click', async () => {
const registration = await navigator.serviceWorker.ready;
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const { publicKey } = await (await fetch('/api/push/key')).json();
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey,
});
await fetch('/api/push/subscriptions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(subscription.toJSON()),
});
});

Store the subscription keyed by user and device, and delete it when a send returns 404 or 410; a stale endpoint that keeps being pushed to is the most common cause of silent delivery failure.

Receiving the message and resubscribing when the endpoint changes

Section titled “Receiving the message and resubscribing when the endpoint changes”

The push handler must end in showNotification(). pushsubscriptionchange fires when the push service rotates the endpoint; the worker resubscribes with the same key and tells the server.

self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title ?? 'Update', {
body: data.body ?? '',
data: { url: data.url ?? '/' },
})
);
});
self.addEventListener('pushsubscriptionchange', (event) => {
event.waitUntil((async () => {
const key = event.oldSubscription?.options.applicationServerKey;
const subscription = await self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
await fetch('/api/push/subscriptions', { method: 'POST', body: JSON.stringify(subscription.toJSON()) });
})());
});

pushsubscriptionchange fires in Firefox 44 (without oldSubscription, bug 1497429), Safari 16, and Chrome 138 (BCD api.ServiceWorkerGlobalScope.pushsubscriptionchange_event); before Chrome 138 a server can only detect rotation by the 410 it receives.

Detecting support and reusing an existing subscription

Section titled “Detecting support and reusing an existing subscription”

Check for PushManager on the registration, then prefer getSubscription() so that a reload does not create a second subscription. The fallback is polling or in-app messages.

async function currentSubscription() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
return null; // no push: poll for updates while the app is open
}
const registration = await navigator.serviceWorker.getRegistration();
if (!registration?.active) return null;
return registration.pushManager.getSubscription(); // existing subscription or null
}

A null here with permission "granted" means the user has not subscribed yet, which is the moment to show the enable button from the first example.

The companion demo at /demo/#push calls Notification.requestPermission() and then pushManager.subscribe() with a VAPID public key generated for the demo, and prints the resulting PushSubscription JSON. There is no server behind it: nothing is sent and no message arrives, which makes it a safe place to see the permission flow and the subscription shape.

Specifications

SpecificationStatus
Web PushW3C
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Android)Yes50mediumsource—
Chrome (Desktop)Yes50mediumsource—
Edge (Desktop)Yes17mediumsource—
Safari (iOS)Partial16.4mediumsource1
Safari (macOS)Yes16mediumsource—
Firefox (Desktop)Yes44mediumsource—
Samsung InternetYes5.0mediumsource—
  1. Only for home-screen-installed web apps; requires user-gesture permission and Web Push via APNs.

Ecosystem & commercial policy

EntityTypeContextStatusSponsoredNotes
Apple Push (APNs)delivery_policyiOSPartialNoiOS Web Push requires the user to add the app to the Home Screen first.

Source data: /compatibility/web-push.json · Global usage: 89 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-06-24 · Confidence: medium (computed from sources)