Notifications · API
Web Push and PushManager.subscribe()
Published
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.
Syntax
Section titled “Syntax”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.
Parameters
Section titled “Parameters”| 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.
Exceptions
Section titled “Exceptions”| 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.
Examples
Section titled “Examples”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.
Try it
Section titled “Try it”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.
See also
Section titled “See also”- Push API: subscribe() method (w3.org)
- RFC 8292: Voluntary Application Server Identification (VAPID) for Web Push (ietf.org)
- Push notifications overview (web.dev)
- Notifications API
- Web Push on iOS and iPadOS
- Notification.requestPermission()
Specifications
| Specification | Status |
|---|---|
| Web Push | W3C |
- Legend
- Yes
- Partial
- Flag
- No
- Unknown
| Browser / Platform | Support | Versions | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Android) | Yes | 50 | medium | source | — |
| Chrome (Desktop) | Yes | 50 | medium | source | — |
| Edge (Desktop) | Yes | 17 | medium | source | — |
| Safari (iOS) | Partial | 16.4 | medium | source | 1 |
| Safari (macOS) | Yes | 16 | medium | source | — |
| Firefox (Desktop) | Yes | 44 | medium | source | — |
| Samsung Internet | Yes | 5.0 | medium | source | — |
- Only for home-screen-installed web apps; requires user-gesture permission and Web Push via APNs.
Ecosystem & commercial policy
| Entity | Type | Context | Status | Sponsored | Notes |
|---|---|---|---|---|---|
| Apple Push (APNs) | delivery_policy | iOS | Partial | No | iOS Web Push requires the user to add the app to the Home Screen first. |