# protocol_handlers manifest member

> protocol_handlers registers an installed web app in the OS as the handler for URL schemes such as mailto or web+note; Chrome 96 and Edge 96, desktop only.

`protocol_handlers` is an array of `{ protocol, url }` objects that asks the browser to register
the installed web app with the operating system as a handler for URL schemes. Once registered,
a `web+note://` or `mailto:` link clicked in any application opens the web app at the `url`,
with `%s` replaced by the full link.

Chrome 96 and Edge 96 implement the member for desktop installs on Windows, macOS, and Linux
(BCD `html.manifest.protocol_handlers`). Chrome on Android, Samsung Internet, Firefox 157, and
Safari 27 do not read it. The runtime API `navigator.registerProtocolHandler()` covers a
different case: it registers the current site, in the browser only, and needs a user gesture
on each visit; the manifest member registers at install time and reaches the OS.

## Member

- **Type**: array of objects. Each object has two required strings: `protocol`, a scheme name
  without the colon, and `url`, an HTTPS URL inside the manifest `scope` that contains the
  literal token `%s`. A relative `url` resolves against the manifest URL.
- **Default**: an empty array; nothing is registered.
- **Example value**: `[{ "protocol": "web+note", "url": "/open?note=%s" }]`.

`protocol` must be either a custom scheme beginning with `web+` followed by lowercase ASCII
letters, or one of the schemes the HTML Standard safelists: `bitcoin`, `ftp`, `ftps`, `geo`,
`im`, `irc`, `ircs`, `magnet`, `mailto`, `matrix`, `mms`, `news`, `nntp`, `openpgp4fpr`, `sftp`,
`sip`, `sms`, `smsto`, `ssh`, `tel`, `urn`, `webcal`, `wtai`, and `xmpp`. Anything else, such
as `note` or `ms-word`, is skipped.

Chromium's parser drops an entry rather than the array when it is malformed:
`protocol_handlers entry ignored, required property 'protocol' is invalid.` and
`protocol_handlers entry ignored, required property 'url' is invalid.` name the missing field,
and a `url` without the token produces `The url provided ('/open?note=') does not contain
'%s'.` The spec also skips an entry whose `url` falls outside `scope` and a second entry whose
normalized `url` duplicates an earlier one.

Registration is per install: Chrome writes the handler into the OS when the app is installed
and removes it on uninstall. The first time a link with the scheme launches the app, Chrome
shows a confirmation dialog naming the app before navigating, and the OS may ask the user to
pick a default when more than one application claims the scheme.

:::observed
Chrome 155 on macOS 26 (English UI), DevTools > Application > Manifest: a **Protocol
Handlers** section lists each declared scheme and offers a text field with a **Test protocol**
button. Typing `web+note://2026-10-03` and pressing it navigates the installed app to
`/open?note=web+note://2026-10-03`: the whole link, not just its payload, stands in for `%s`,
with only characters such as `#` percent-encoded (the Manifest Incubations example shows
`web+music://#1234` arriving as `web+music://%231234`).
:::

## Examples

The manifest side is a few lines; the handler page has to cope with a value that may be
absent, encoded, or not the scheme it expected.

### Registering a custom `web+` scheme for a notes app

A notes app claims `web+note` so that `web+note://2026-10-03` links in email and chat open the
matching entry. The `url` sits under the app's `scope` and carries `%s` as a query value, which
keeps the rest of the path static.

```json
{
  "name": "Notebook",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "protocol_handlers": [
    { "protocol": "web+note", "url": "/open?note=%s" }
  ]
}
```

Only one handler per scheme is registered; listing the same `protocol` twice with different
URLs keeps the first entry.

### Decoding the substituted link on the handler page

The browser replaces `%s` with the entire link, so the page receives `web+note://2026-10-03`,
not `2026-10-03`, with characters such as `#` percent-encoded. Read the raw query value and
decode it yourself: `URLSearchParams` turns `+` into a space and would corrupt `web+note`. The fallback branch
renders the ordinary page when the value is missing or carries another scheme, which is what a
direct visit to `/open` looks like.

```js
const raw = location.search.match(/[?&]note=([^&]*)/)?.[1];
let link = null;
try {
  link = raw ? decodeURIComponent(raw) : null;
} catch {
  link = null; // malformed percent-encoding: treat as no link
}

if (link?.startsWith('web+note://')) {
  openNote(link.slice('web+note://'.length)); // "2026-10-03"
} else {
  showNoteList(); // direct visit, or a scheme this page does not handle
}
```

Replace the query with `history.replaceState()` after reading it so a reload or a bookmark does
not re-run the handler.

### Falling back to the runtime API where the manifest is ignored

On browsers without the manifest member, `navigator.registerProtocolHandler()` registers the
same scheme inside the browser only, and must be called from a user gesture. Feature-detect
both and expose a button for the runtime path.

```js
export function offerProtocolRegistration(button) {
  const installed = matchMedia('(display-mode: standalone)').matches;
  if (installed) return; // Chrome/Edge desktop: the manifest already registered web+note
  if (typeof navigator.registerProtocolHandler !== 'function') return; // Safari: no path
  button.hidden = false;
  button.addEventListener('click', () => {
    navigator.registerProtocolHandler('web+note', `${location.origin}/open?note=%s`);
  });
}
```

Firefox 157 accepts the runtime registration for `web+` schemes; the handler then applies to
links clicked inside Firefox, not to links in other applications.

## See also

- [Manifest protocol_handlers: registering a PWA for custom URL schemes](/reference/capabilities/protocol-handlers/)
- [scope manifest member](/reference/manifest/scope/)
- [launch_handler manifest member](/reference/manifest/launch-handler/)
- [Manifest Incubations: protocol_handlers member](https://wicg.github.io/manifest-incubations/#protocol_handlers-member) (wicg.github.io)
- [Handle protocols in PWAs](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps/how-to/handle-protocols) (learn.microsoft.com)
- [Navigator: registerProtocolHandler() method](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler) (developer.mozilla.org)