Skip to content

iOS and Safari Web Push (16.4+)

Published Updated

In one line: Web Push is the W3C combination of the Push API, the Notifications API and service workers that lets a site notify a user when the site is not open; WebKit added it for Home Screen web apps on iOS and iPadOS 16.4, where the permission request must come from direct user interaction and the subscription must promise a visible notification. Since iOS and iPadOS 18.4 there is a second flavour — Declarative Web Push — which subscribes and displays notifications without requiring an installed service worker.

  • Standards-based push. WebKit implemented the same W3C standards it shipped on macOS, so an app already coded to the standards — using feature detection instead of browser detection — works on iPhone and iPad automatically.
  • iOS/iPadOS requires a Home Screen web app. A web app added to the Home Screen can request permission to receive push notifications; that is the surface WebKit added push to.
  • Notifications behave like any other app’s. They appear on the Lock Screen, in Notification Center and on a paired Apple Watch, and users manage them per web app in Notifications Settings.
  • Focus and Badging came with it. Home Screen web app notifications integrate with Focus, and 16.4 added the Badging API so a web app can set its icon badge count.
  • APNs does the delivery. Web Push on iOS and iPadOS uses the same Apple Push Notification service that powers native push. You do not need to be a member of the Apple Developer Program, but if you control your server’s push endpoints, allow URLs from *.push.apple.com.

iOS and iPadOS 16.4 is the floor on iPhone and iPad: support arrived with that release, for web apps added to the Home Screen. On the desktop the timeline is earlier and the install step does not apply — WebKit’s iOS/iPadOS 16.4 article identifies the macOS release as Safari 16.1 on macOS Ventura (the earlier announcement said Safari 16), where the implementation relies on a system daemon (webpushd) so a push can be delivered even when Safari is not running.

  • 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)

What “16.4, Home Screen only” looks like in code

Section titled “What “16.4, Home Screen only” looks like in code”

MDN’s browser-compat-data records the iOS/iPadOS entry for the Notification interface as 16.4, partial, and the two notes it attaches are the concrete shape of the install gate:

  • The interface is undefined unless the page is a web app saved to the Home Screen, and the app’s manifest must have a non-default display value. So on iOS this is not a “permission denied” situation you can recover from — in a plain Safari tab the symbol is not there at all, and touching the constructor throws a ReferenceError.
  • A notification can only be sent from a service worker on iOS and iPadOS: use ServiceWorkerRegistration.showNotification(), not new Notification().

Those two notes explain why a push integration that works on desktop Safari can appear to be completely absent on iPhone: the manifest display value is doing load-bearing work, and a page-scoped new Notification() has no supported path on the platform at all.

This is the service-worker-based path WebKit shipped in 16.4. For the service-worker-free alternative added in 18.4, see Declarative Web Push below.

  1. Install first. The user adds your web app to the Home Screen from the Share menu. In 16.4 third-party browsers can offer “Add to Home Screen” too, and a site whose manifest sets display to standalone or fullscreen opens as a web app no matter which browser added it.
  2. Ask from direct user interaction. Inside the installed web app, call Notification.requestPermission() in response to a real interaction — WebKit describes this as a tap on a “subscribe” button the web app provides. iOS or iPadOS then shows the system prompt.
  3. Subscribe with VAPID. Call pushManager.subscribe() with userVisibleOnly: true and your application server key, and persist the returned PushSubscription (its endpoint plus the p256dh and auth keys) server-side.
  4. Send and show. Your server posts an encrypted message to the endpoint; the push event starts your service worker, where you call showNotification().

The permission request is the first thing that can fail, and on iOS it fails hard: as the row above records, the Notification interface is undefined in a plain tab, so Notification.requestPermission() throws a ReferenceError before any subscribe() call — a try wrapped around subscribe() alone never sees it. Check the interface first, and keep the permission request inside the same guarded path:

// Inside the installed web app, from a user-interaction handler:
async function enablePush(swReg, applicationServerKey) {
// On iOS/iPadOS the interface is absent outside a Home Screen web app, so reading
// it in a plain tab throws a ReferenceError. Route that case to install guidance.
if (!('Notification' in window)) {
showAddToHomeScreenGuidance();
return null;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') return null;
try {
return await swReg.pushManager.subscribe({
userVisibleOnly: true, // required: every push must show a notification
applicationServerKey, // VAPID key — no APNs key in client code
});
} catch (error) {
// A NotAllowedError can mean denied permission, a non-HTTPS window scope, or
// userVisibleOnly: false where the user agent requires true. It does not establish
// that the app is not installed; the Notification guard above detects the iOS gate.
reportSubscriptionFailure(error);
return null;
}
}

The second flavour: Declarative Web Push (18.4+)

Section titled “The second flavour: Declarative Web Push (18.4+)”

The steps above describe original Web Push, the JavaScript-first design in which a service worker registration creates the subscription and a PushEvent handler calls showNotification(). WebKit shipped Declarative Web Push on iOS and iPadOS 18.4 for web apps added to the Home Screen, and on macOS from Safari 18.5 (the release that shipped with macOS 15.5). It lets you request a push subscription and display user-visible notifications without requiring an installed service worker.

Two differences matter when you write code:

  • A different PushManager. The only PushManager available with original Web Push is ServiceWorkerRegistration.pushManager. Declarative Web Push also exposes window.pushManager, to support subscription management without a service worker. If you do also have a service worker registered at the root scope of your domain, it shares the same push subscription as the window object — and removing that registration does not affect the subscription.
  • A declarative message format. For a notification to be handled declaratively, the push message must match the declarative standard JSON format (a "web_push": 8030 member plus a notification object), so the browser has enough information to display the notification without any JavaScript. Service worker JavaScript may optionally change the contents of an incoming notification.
// Declarative Web Push: no service worker registration required.
const subscription = await window.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: arrayForPublicKey,
});

Feature-detect rather than sniffing for Safari — WebKit’s own guidance is that an app using feature detection picks up push on iPhone and iPad as soon as it ships. Prefer the declarative, window-scoped manager when it is there, and only then fall back to the service-worker-scoped one that original Web Push requires:

Probe for a usable manager, not for the PushManager interface: the presence of the global interface does not by itself mean this context can subscribe. Note what this does not do — no client-side probe below establishes Home Screen status, so on iOS a plain Safari tab can pass these checks and still fail later. The Notification guard in enablePush() above is what catches that tab; a subscribe() rejection is not a reliable install signal on its own, because the same rejection channel carries key and subscription errors (see the error-name split above).

async function getPushManager() {
// Declarative Web Push (18.4+): subscribe without an installed service worker.
if ('pushManager' in window) return window.pushManager;
if (!('serviceWorker' in navigator)) {
// Nothing to subscribe with at all. Fall back to in-app messaging and show
// install guidance instead of failing.
showAddToHomeScreenGuidance();
return null;
}
// Original Web Push: the subscription lives on the service worker registration.
// Ask for the registration — `navigator.serviceWorker.ready` never rejects and waits
// indefinitely for an active worker, which would hang here instead of reaching the
// guidance below. Require `active`: subscribe() rejects with "InvalidStateError"
// when the registration has no active worker.
const swReg = await navigator.serviceWorker.getRegistration();
if (!swReg || !swReg.active || !('pushManager' in swReg)) {
showAddToHomeScreenGuidance();
return null;
}
return swReg.pushManager;
}

navigator.serviceWorker.ready is the wrong probe here even though the API exists. MDN documents it as a promise that never rejects and waits indefinitely until the page’s registration has an active worker. On a context that exposes the service worker API but has no registration — or has one holding only an installing/waiting worker, which the Service Worker specification lets become redundant if installation fails — and no window.pushManager, awaiting it stalls the function forever and the install guidance promised above never appears. getRegistration() settles either way (MDN: it resolves to a ServiceWorkerRegistration or undefined), and ServiceWorkerRegistration.active is null until a worker reaches activating/activated — so reading active gives you a bounded answer where ready gives you a wait. If you genuinely need to wait for activation, watch the worker’s statechange for activated versus redundant behind your own timeout rather than awaiting ready.

  • The prompt needs an explicit user gesture. Requesting a push subscription requires one, so a prompt fired on page load has nowhere to land. Ask in context, after the user does something push helps with.
  • Silent push is not allowed. userVisibleOnly: true is required in both flavours. Under original Web Push you must keep that promise in JavaScript: if a PushEvent handler does not show the user-visible notification, for any reason, WebKit revokes the subscription — and a service worker bug, network conditions or local device conditions can all prevent a timely showNotification() call. Under Declarative Web Push there is no such penalty for a service worker failing to display a notification, because the declarative push message itself is used as a fallback when the optional JavaScript step fails. Push is not a licence for silent background runtime either way.
  • The endpoint is a capability URL. MDN’s warning is that knowledge of the endpoint is all that is necessary to send a message to your application, so it needs to be kept secret — otherwise other applications might be able to send push messages to your app.
  • Budget for delivery limits. Waking a service worker costs battery, and browsers differ: Firefox applies a quota to push messages that do not generate notifications, Chrome sets no limit, and there is no standard mechanism.
  • Test on real hardware. The install gate, the prompt and delivery are system-level behaviours — verify on an actual iPhone or iPad running 16.4 or later.
Decision question Recommended action Rationale
Can I push to an iOS user in a browser tab? No — require the Home Screen web app first. 16.4 added Web Push for web apps added to the Home Screen.
What key do I subscribe with? A standard VAPID applicationServerKey. WebKit implements the same W3C Push API; no APNs key client-side.
Do I need an Apple Developer Program membership? No. WebKit states membership is not required to send Web Push.
When do I prompt? From direct user interaction, inside the installed web app. A push subscription request requires an explicit user gesture.
macOS Safari without install? Supported since Safari 16.1 on macOS Ventura. The Home Screen step is the iOS/iPadOS path.
Can I push without showing a notification? No. userVisibleOnly: true is mandatory; under original Web Push a handler that fails to show one has its subscription revoked.
Do I have to register a service worker? Not for Declarative Web Push on 18.4+. Declarative Web Push subscribes and displays notifications without an installed service worker; original Web Push has only ServiceWorkerRegistration.pushManager.
  • Gate the push prompt behind a Home Screen web app on iOS/iPadOS, and show install guidance otherwise.
  • Request permission only from direct user interaction, in context.
  • Subscribe with userVisibleOnly: true and a VAPID applicationServerKey.
  • Persist the full PushSubscription (endpoint + p256dh + auth) server-side, and treat the endpoint as a secret.
  • On the original Web Push path, always call showNotification() in the push handler — a missed notification can cost you the subscription.
  • To go declarative on 18.4+, subscribe via window.pushManager and send messages in the declarative JSON format, so no service worker is required.
  • Allow *.push.apple.com if you restrict outbound push endpoints.
  • Use feature detection, never browser detection — and probe for the manager you will actually use (window.pushManager for the declarative path, the registration’s pushManager for the original one) rather than treating the bare PushManager interface as proof of availability in this context.
  • Resolve the registration with navigator.serviceWorker.getRegistration() and require registration.active, rather than awaiting navigator.serviceWorker.ready: ready never rejects and waits indefinitely for an active worker, so the probe hangs instead of showing install guidance.
  • Check 'Notification' in window before calling Notification.requestPermission() — in a plain iOS tab the interface is undefined and reading it throws a ReferenceError that a try around subscribe() will never catch.
  • Do not treat a subscribe() rejection as an installation signal. NotAllowedError can mean denied push permission, a non-HTTPS window scope, or userVisibleOnly: false where the user agent requires true. On iOS, use the absence of the Notification interface outside an eligible Home Screen web app for the install guidance branch.
  • Give the manifest a non-default display value — on iOS the Notification interface stays undefined without one.
  • Show notifications from the service worker (registration.showNotification()); new Notification() has no supported path on iOS or iPadOS.
  • Verify the whole flow on a real iOS 16.4+ device.
  • Web Push — the cross-browser protocol, VAPID keys and server-side send path this page builds on.
  • iOS Add to Home Screen — the install step that unlocks push on iPhone and iPad.
  • Notification permissions — how to ask, and what a denied answer means.
  • The Notifications API — the display half, and the persistent-vs-non-persistent distinction the iOS service-worker requirement follows from.