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.
What shipped in 16.4
Section titled “What shipped in 16.4”- 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.
Where it is supported
Section titled “Where it is supported”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 / 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. |
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
displayvalue. 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 aReferenceError. - A notification can only be sent from a service worker on iOS and iPadOS: use
ServiceWorkerRegistration.showNotification(), notnew 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.
The flow on iOS (original Web Push)
Section titled “The flow on iOS (original Web Push)”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.
- 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
displaytostandaloneorfullscreenopens as a web app no matter which browser added it. - 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. - Subscribe with VAPID. Call
pushManager.subscribe()withuserVisibleOnly: trueand your application server key, and persist the returnedPushSubscription(itsendpointplus thep256dhandauthkeys) server-side. - Send and show. Your server posts an encrypted message to the
endpoint; thepushevent starts your service worker, where you callshowNotification().
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 onlyPushManageravailable with original Web Push isServiceWorkerRegistration.pushManager. Declarative Web Push also exposeswindow.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 thewindowobject — 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": 8030member plus anotificationobject), 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,});Detecting support and falling back
Section titled “Detecting support and falling back”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.
Constraints to design around
Section titled “Constraints to design around”- 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: trueis required in both flavours. Under original Web Push you must keep that promise in JavaScript: if aPushEventhandler 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 timelyshowNotification()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 framework
Section titled “Decision framework”| 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. |
Practical checklist
Section titled “Practical checklist”- 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: trueand a VAPIDapplicationServerKey. - 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 thepushhandler — a missed notification can cost you the subscription. - To go declarative on 18.4+, subscribe via
window.pushManagerand send messages in the declarative JSON format, so no service worker is required. - Allow
*.push.apple.comif you restrict outbound push endpoints. - Use feature detection, never browser detection — and probe for the manager you will actually use (
window.pushManagerfor the declarative path, the registration’spushManagerfor the original one) rather than treating the barePushManagerinterface as proof of availability in this context. - Resolve the registration with
navigator.serviceWorker.getRegistration()and requireregistration.active, rather than awaitingnavigator.serviceWorker.ready:readynever rejects and waits indefinitely for an active worker, so the probe hangs instead of showing install guidance. - Check
'Notification' in windowbefore callingNotification.requestPermission()— in a plain iOS tab the interface is undefined and reading it throws aReferenceErrorthat atryaroundsubscribe()will never catch. - Do not treat a
subscribe()rejection as an installation signal.NotAllowedErrorcan mean denied push permission, a non-HTTPS window scope, oruserVisibleOnly: falsewhere the user agent requires true. On iOS, use the absence of theNotificationinterface outside an eligible Home Screen web app for the install guidance branch. - Give the manifest a non-default
displayvalue — on iOS theNotificationinterface 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.
Where to go next
Section titled “Where to go next”- 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.