# Web Install API (navigator.install)

> How navigator.install() installs the current document or a manifest URL, the activation and permission it needs, and the DOMException names it rejects with.

`navigator.install()` is a promise-based method that asks the browser to install a web app:
the manifest linked by the current document when it is called with no argument, or the manifest
at a URL the caller supplies. The explainer positions it as an extension of
`beforeinstallprompt`, which cannot install content other than the web application already
loaded.

## Support position

BCD `api.Navigator.install` records support from Chrome 154 behind a preference flag named
`#web-app-installation-api` set to `Enabled`, mirrored into the Chromium-based browsers the
record lists, and no support in Firefox or Safari. The record is marked experimental and
standard-track.

[Chrome Platform Status feature 5183481574850560](https://chromestatus.com/feature/5183481574850560)
(chromestatus.com) lists the Web Install API as enabled by default in Chrome 157, and summarises
it as "Starting in Chrome 157, the new Web Install API
allows websites to prompt users to install a web app, given a manifest URL and optional manifest
ID." The same page records the specification maturity as "Specification being incubated in a
Community Group", Mozilla's standards position as "No signal", and WebKit's as "Oppose", each
linking the corresponding standards-positions issue.

## Syntax

```js
await navigator.install();
await navigator.install({ manifest: 'https://suite.example/mail/manifest.json' });
await navigator.install({
  manifest: 'https://music.example/manifest.webmanifest',
  manifestId: 'https://music.example/app',
});
```

The draft specification declares two overloads on a partial `Navigator` interface, both returning
`Promise<WebInstallResult>`: one with no argument and one taking an `InstallParams` dictionary
whose `manifest` member is required and whose `manifestId` member is an optional, nullable
`USVString`. `WebInstallResult` is declared as an empty dictionary.

## Parameters

The dictionary may be omitted entirely, in which case the manifest of the page already loaded is
the install target.

| Parameter | Type | Meaning |
|---|---|---|
| `options.manifest` | `USVString`, required | The URL of the application manifest identifying the target web application. Relative values are resolved against the initiating document's URL. |
| `options.manifestId` | `USVString?`, optional | An absolute manifest ID assertion: the processed manifest id the caller expects. The draft states that null and omission have the same meaning. |

The explainer states that the developer may omit `manifestId` if and only if the JSON at
`manifest` contains an `id`. Where a manifest declares no `id` member, the
[Web App Manifest](https://www.w3.org/TR/appmanifest/#id-member) (w3.org) computes the id from
the `start_url`, and the explainer points at the Application tab of Chromium DevTools as the
place that computed id is readable.

The promise resolves with a `WebInstallResult`, which the draft defines as an empty dictionary
for future compatibility.

## Exceptions

The method is declared to return `Promise<WebInstallResult>`, so each failure below reaches the
caller as a promise rejection: handle them with `await` inside `try`/`catch`, or with a rejection
handler on the returned promise. The draft's method steps use the specification's `throw` wording
for the first three, which Web IDL turns into a rejected promise for a promise-returning
operation, and the explainer's own steps describe those same names as names the promise rejects
with. The draft notes that user activation is consumed before argument validation, which matches
Chromium, so an unparseable URL still spends the activation.

| Rejection | Raised at | When |
|---|---|---|
| `InvalidStateError` | the method steps, before any request starts | The document is not an eligible initiating document. The draft requires a fully active document that is the active document of a top-level traversable, is not a fenced frame document, has an empty active sandboxing flag set, and has the `web-app-installation` policy-controlled feature enabled. |
| `NotAllowedError` | the method steps, before any request starts | The document has no transient activation. |
| `TypeError` | the method steps, before any request starts | `options.manifest` fails to parse against the document's URL, or `options.manifestId` fails to parse as an absolute URL. |
| `DataError` | the request outcome | The outcome was invalid install data: per the draft, the supplied URL, manifest, identity assertion, or required installation metadata was invalid or unavailable. The explainer lists an invalid `manifest` URL, a failed manifest fetch, a malformed manifest, an omitted `manifestId` where the manifest declares no `id`, and a `manifestId` that does not match the computed one. |
| `AbortError` | the request outcome | Every other non-completion. The draft's note states that "Permission denial, unsupported profiles, navigation, concurrency, user dismissal, and internal failure all map to AbortError." |

The explainer also lists `NotFoundError` for a missing document among the names the promise can
reject with, while the draft's provisional method steps route every ineligible initiating
document to `InvalidStateError` instead; the draft marks that taxonomy as unresolved.

One further rejection path has no name of its own: the draft gives each top-level traversable a
single active web install request, so a second call made while a dialog is open finishes with
installation aborted.

:::caution
Chrome Platform Status describes two administrator controls over this API: the enterprise policy
[`WebAppInstallByUserEnabled`](https://chromeenterprise.google/policies/#WebAppInstallByUserEnabled)
(chromeenterprise.google), which disables user web app installs broadly, and the
`web-app-installation` permissions policy, which allows or disallows the feature on origins the
enterprise controls. When the `web-app-installation` policy-controlled feature is disabled, the
document is ineligible and `navigator.install()` rejects with `InvalidStateError` before a request
starts. Permission or other policy denial while processing a request and user dismissal map to
`AbortError`.
:::

## Examples

Each example runs from a `click` handler, because the draft rejects with `NotAllowedError` without
transient activation. The dictionary form additionally goes through the `web-app-installation`
permission.

### Installing the current page from a button

The no-argument form targets the manifest the document links. In the draft's
"obtain web install permission" steps a current-document source returns true without requesting
the `web-app-installation` permission, so the flow reaches the browser's confirmation UI
directly. A manifest with no `id` member resolves one through `start_url`, but the explainer's
no-argument install steps reject with `DataError` when the manifest file declares no `id`, so
declare one.

```js
const button = document.querySelector('#install');

button.addEventListener('click', async () => {
  try {
    await navigator.install();
    button.hidden = true;
  } catch (err) {
    if (err.name === 'DataError') {
      // Manifest missing, unfetchable, or declaring no `id`.
      reportInstallBlocked('manifest');
    } else if (err.name === 'AbortError') {
      // Dismissed, denied by policy, or an unsupported profile.
      button.textContent = 'Install';
    }
  }
});
```

Branch on `err.name` rather than on a message string: the names above are the ones the draft and
the explainer define, and the messages are not specified.

### Installing a sibling app from a catalog page

The dictionary form names another app's manifest. The explainer's steps fetch that manifest with
credentials mode `"omit"`, and its open questions record the current decision that the manifest
URL be same-origin with `start_url`, with relative URLs inside the manifest resolved against the
manifest URL. Pass `manifestId` when the target manifest declares no `id` of its own.

```js
const CATALOG = [
  { name: 'Mail', manifest: 'https://suite.example/mail/manifest.json' },
  { name: 'Calendar', manifest: 'https://suite.example/calendar/manifest.json' },
];

async function installFromCatalog(entry) {
  try {
    await navigator.install({ manifest: entry.manifest });
    return { ok: true };
  } catch (err) {
    if (err.name === 'TypeError' || err.name === 'DataError') {
      return { ok: false, reason: 'catalog-entry-stale' };
    }
    return { ok: false, reason: 'not-installed' }; // AbortError and anything else
  }
}
```

A stale catalog entry is the case `DataError` exists for: the explainer's open questions keep
versioned manifest filenames unsolved and rely on `DataError` to tell the caller a published URL
has broken.

### Detecting support and falling back to beforeinstallprompt

The permission query tells a page whether a cross-origin install would be refused before it
renders a button, and `'install' in navigator` tells it whether the method exists at all. Where
it does not, the deferred `beforeinstallprompt` event is the Chromium path that does, and the
remaining browsers have their own install surfaces instead.

```js
let deferredPrompt = null;
addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();
  deferredPrompt = event;
});

async function installTarget(manifestUrl) {
  if (!('install' in navigator)) {
    if (deferredPrompt) return deferredPrompt.prompt(); // Chromium's older entry point
    return { ok: false, reason: 'use-browser-menu' }; // no scripted install here
  }
  const status = await navigator.permissions
    .query({ name: 'web-app-installation' })
    .catch(() => null); // name unsupported: TypeError per the Permissions spec
  if (status?.state === 'denied') {
    return { ok: false, reason: 'permission-denied' };
  }
  return navigator.install({ manifest: manifestUrl });
}
```

BCD `api.Permissions.permission_web-app-installation` records the permission name from Chrome 154
behind the same preference flag, so the `catch` branch covers the browsers where the name is not
registered and `query()` rejects with a `TypeError`.

:::observed
Exact strings, each from the source named. BCD `api.Navigator.install` gives the Chrome
preference flag as `#web-app-installation-api` with `value_to_set` of `Enabled`
([api/Navigator.json](https://github.com/mdn/browser-compat-data/blob/main/api/Navigator.json),
github.com). The draft's declarative counterpart defines `enum InstallResult { "success",
"aborted", "invalid_data" };` and surfaces it through an `InstallResultEvent`
([Web Install](https://wicg.github.io/install-element/#install-result-event), wicg.github.io),
so the `<install>` element reports the same three outcomes the promise maps to resolve,
`AbortError`, and `DataError`. Chrome Platform Status names the two administrator controls
`WebAppInstallByUserEnabled` and `web-app-installation`
([feature 5183481574850560](https://chromestatus.com/feature/5183481574850560), chromestatus.com).
:::

## See also

- [Web Install](https://wicg.github.io/install-element/#navigator-api) (wicg.github.io)
- [Web Install API explainer](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/WebInstall/explainer.md) (github.com)
- [beforeinstallprompt event](/reference/installation/install-prompt/)
- [Installability criteria](/reference/installation/installability-criteria/)