# Web Push on iOS and iPadOS (16.4+)

> Web Push for Home Screen web apps on iOS 16.4: the install requirement, the missing Notification interface in a tab, APNs delivery, and Declarative Web Push.

iOS and iPadOS 16.4 (2023-03) added Web Push for web apps added to the Home Screen: the same
Push API, Notifications API, and service worker combination as desktop, delivered through the
Apple Push Notification service, and available only after the user installs the app and taps a
control that requests permission. In a Safari tab on iPhone the `Notification` interface does
not exist, so an uninstalled site cannot even ask.

## How it works

WebKit gates the whole feature on installation. A site becomes a Home Screen web app when the
user chooses "Add to Home Screen" from the Share menu and the manifest's `display` member is
`standalone` or `fullscreen` (or `minimal-ui`; any non-default value); without that manifest value
the icon is a bookmark that opens in Safari, where push is unavailable ([Web Push for Web Apps on
iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/),
webkit.org). From 16.4 third-party browsers can offer the same Share-menu item, and the resulting
app opens as a web app regardless of which browser added it.

### What a Safari tab exposes

Browser-compat-data records the iOS entry for `Notification` as 16.4, partial, with two notes:
the interface is undefined unless the page is a web app saved to the Home Screen with a
non-default manifest `display`, and a notification can only be sent from a service worker
(`api.Notification`, `safari_ios`). Both have code consequences. `Notification.requestPermission()`
in a tab throws `ReferenceError`, not a rejected promise, and `new Notification()` has no
supported path on the platform at all; `registration.showNotification()` is the only way to
display one.

### Permission, subscription, and delivery

Inside the installed app, the permission request must follow direct user interaction, such as a
tap on a subscribe button; iOS then shows the system notification prompt, and the user manages
the result per web app under Settings > Notifications like any native app. `pushManager.subscribe()`
requires `userVisibleOnly: true` and a VAPID `applicationServerKey`; the returned endpoint is on a
`*.push.apple.com` host, which servers that allow-list push endpoints must permit. WebKit revokes a
subscription whose `push` events do not result in a visible notification ([Meet Web
Push](https://webkit.org/blog/12945/meet-web-push/), webkit.org), so a handler that swallows a
message, or fails before `showNotification()`, costs the subscription rather than one alert.

Notifications integrate with Focus, and Focus settings sync across devices for web apps whose
manifest `id` and user-chosen name match. The Badging API (`navigator.setAppBadge()`) shipped in
the same release for Home Screen web apps, with the badge shown once notification permission is
granted.

### Declarative Web Push (18.4+)

iOS and iPadOS 18.4 added a second flavour for Home Screen web apps, and Safari 18.5 brought it to
macOS: `window.pushManager` (BCD `api.Window.pushManager`: Safari 18.4) lets a page subscribe
without a service worker, and a push message in the declarative JSON format (`"web_push": 8030`
plus a `notification` object) is displayed by the browser with no JavaScript. A service worker, if
present, may still modify the notification; if its handler fails, the declarative message is shown
as the fallback, so the revocation rule above does not bite on this path ([Meet Declarative Web
Push](https://webkit.org/blog/16535/meet-declarative-web-push/), webkit.org).

### Support position

Safari 16 on macOS Ventura shipped Web Push first (2022-10), with delivery through the `webpushd`
daemon so that Safari need not be running. iOS and iPadOS 16.4 followed for Home Screen web apps
only, and the `compat` table on this page tracks both. Android WebView and iOS WebViews inside
other apps do not get push; a Chromium-based browser on Android gets it in any tab.

## Examples

The examples run inside the installed app on iOS and in any tab elsewhere; the first one is the guard that tells the two apart.

### Routing an uninstalled iPhone visitor to install guidance

The `'Notification' in window` test is the install check on iOS: it is `false` in a tab and `true`
in the Home Screen app. The same test is also the right first step everywhere else, so no
platform sniffing is needed.

```js
async function enablePush(registration, applicationServerKey) {
  if (!('Notification' in window)) {
    showAddToHomeScreenGuidance(); // iOS tab or embedded web view: install first
    return null;
  }
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return null;
  return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
}
```

Call `enablePush()` from a tap handler; on iOS a call outside direct user interaction resolves
`"denied"` without a prompt.

### Keeping the userVisibleOnly promise in the push handler

Every `push` event must end in `showNotification()`, including the error path, or WebKit revokes
the subscription. A generic fallback notification is safer than none.

```js
self.addEventListener('push', (event) => {
  event.waitUntil((async () => {
    let payload = { title: 'Update available', body: '' };
    try {
      payload = event.data.json();
    } catch {
      // malformed payload: still show something, or the subscription is revoked
    }
    await self.registration.showNotification(payload.title, { body: payload.body });
  })());
});
```

The `try`/`catch` matters on iOS more than elsewhere: a server bug that sends a malformed body
would otherwise silently unsubscribe every iPhone user.

### Preferring the declarative manager when present

Probe for the manager you will use rather than for the `PushManager` interface. `window.pushManager`
exists only on the declarative path; a service worker registration's `pushManager` requires an
active worker.

```js
async function getPushManager() {
  if ('pushManager' in window) return window.pushManager; // Declarative Web Push, Safari 18.4+
  if (!('serviceWorker' in navigator)) return null; // no push at all: in-app messaging only
  const registration = await navigator.serviceWorker.getRegistration();
  return registration?.active ? registration.pushManager : null;
}
```

`getRegistration()` settles with `undefined` when nothing is registered; `navigator.serviceWorker.ready`
would wait forever in that case, which is why the probe avoids it.

:::observed
In Safari on iOS 16.4 or later, opening the same URL in a tab and as a Home Screen web app gives
different globals: in the tab `typeof Notification` evaluates to `"undefined"` and
`Notification.requestPermission()` throws `ReferenceError: Can't find variable: Notification`
(WebKit's standard wording for an undefined identifier), while in the installed app the static
exists and the prompt appears after a tap. The difference is recorded as the `safari_ios` notes on
`api.Notification` in [Notification.json](https://raw.githubusercontent.com/mdn/browser-compat-data/main/api/Notification.json)
(github.com) and is the quickest way to confirm that a manifest's `display` value is being honoured.
:::

## See also

- [Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/) (webkit.org)
- [Meet Declarative Web Push](https://webkit.org/blog/16535/meet-declarative-web-push/) (webkit.org)
- [Push API](https://w3c.github.io/push-api/) (w3.org)
- [Web Push and PushManager.subscribe()](/reference/notifications/web-push/)
- [Add to Home Screen on iOS](/reference/installation/ios-add-to-home-screen/)
- [Notification.requestPermission()](/reference/notifications/permissions/)
- [Badging API](/reference/installation/badging/)