# registerProtocolHandler() and manifest protocol handlers

> navigator.registerProtocolHandler() and manifest protocol_handlers share one rule set; web+ scheme rules, the %s placeholder, SecurityError and SyntaxError.

Two routes register a web app as the handler for a URL scheme such as `mailto:` or `web+music:`. `navigator.registerProtocolHandler(scheme, url)` asks from a page, prompts the user, and works in an ordinary tab; the manifest `protocol_handlers` member registers with the operating system when the PWA is installed and needs no prompt from the page. Both run the HTML Standard's "normalize protocol handler parameters" steps, so the scheme and URL rules are the same and a value that throws from the method is silently dropped from the manifest.

`registerProtocolHandler()` has been in Firefox since 2 and Chrome since 13 on desktop, with Edge 79 and Opera 11.6 following; Chrome 77 restricted `url` to `http:` and `https:`. Safari has no implementation, and no Android browser exposes it (BCD `api.Navigator.registerProtocolHandler`). `unregisterProtocolHandler()` exists in Chromium only, since Chrome 38. The manifest member is a desktop-only feature of Chrome 96 and Edge 96; see the compatibility table for the full position.

## Syntax

```js
navigator.registerProtocolHandler(scheme, url)
navigator.unregisterProtocolHandler(scheme, url)
```

```json
{
  "protocol_handlers": [
    { "protocol": "web+music", "url": "/play?song=%s" }
  ]
}
```

Both methods return `undefined`. Registration happens in parallel after the call returns, so there is no promise to await and no way to learn whether the user accepted the prompt. Both are `[SecureContext]`, which the same-origin rule on `url` implies in any case. When a link with the registered scheme is activated, the browser percent-encodes the full link, substitutes it for `%s`, and navigates a new top-level browsing context to the result.

## Parameters

The method arguments and the manifest entry members map one to one.

| Method argument | Manifest member | Type | Required | Description |
|---|---|---|---|---|
| `scheme` | `protocol` | `DOMString` | Yes | Lowercased before checking. Must be a safelisted scheme or start with `web+` followed by one or more lowercase ASCII letters (`web+music`, not `web+music2` or `web+`). A trailing colon makes it invalid. |
| `url` | `url` | `DOMString` | Yes | Must contain the literal `%s`. Parsed relative to the document (method) or the manifest URL (member); the result must be an `http:` or `https:` URL on the document's origin, and for the manifest also within the manifest's `scope`. |

The safelisted schemes are `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`. Manifest processing skips an entry that lacks `protocol` or `url`, fails normalisation, falls outside scope, or repeats a `url` already registered by an earlier entry; it does not report why.

## Exceptions

`registerProtocolHandler()` and `unregisterProtocolHandler()` throw synchronously. The manifest member throws nothing; invalid entries are dropped during processing.

| Exception | Condition |
|---|---|
| `SecurityError` | `scheme` is neither safelisted nor `web+` plus lowercase letters, including `"mailto:"` with its colon; the parsed `url` is not `http:` or `https:` or is not same-origin with the document; the browser blocks the registration, for example for `http` itself. Chrome also throws from Isolated Web Apps, directing authors to the manifest member. |
| `SyntaxError` | `url` does not contain `%s`; `url` fails to parse, which is forced when `%s` sits in the host or port. |

:::observed
Chrome reports the failed check in the exception message. `navigator.registerProtocolHandler("music", "/play?song=%s")` throws `SecurityError: Failed to execute 'registerProtocolHandler' on 'Navigator': The scheme 'music' doesn't belong to the scheme allowlist. Please prefix non-allowlisted schemes with the string 'web+'.`, a `url` without the placeholder throws `SyntaxError: Failed to execute 'registerProtocolHandler' on 'Navigator': The url provided ('/play') does not contain '%s'.`, and a handler on another origin throws `SecurityError: ... Can only register custom handler in the document's origin.` The messages are built in Chromium's [`navigator_content_utils.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/navigatorcontentutils/navigator_content_utils.cc) (chromium.googlesource.com).
:::

## Examples

The examples register `web+music` for a player at `https://player.example/`. The handler route is `/play?song=%s`, so a click on `web+music:track/42` opens `/play` on that origin with a `song` parameter that `URLSearchParams` decodes back to `web+music:track/42`.

### Offering registration from a settings page with a manual fallback

Feature-detect the method and catch both exception names: a `SecurityError` here usually means a typo in the scheme, and the user may decline the prompt without the page ever knowing. Safari and every Android browser take the fallback branch, which shows the user how to paste a link instead.

```js
const button = document.querySelector("#register-handler");

if (typeof navigator.registerProtocolHandler !== "function") {
  button.hidden = true;
  document.querySelector("#paste-link-help").hidden = false;
} else {
  button.addEventListener("click", () => {
    try {
      navigator.registerProtocolHandler("web+music", "/play?song=%s");
      button.textContent = "Requested; accept the browser prompt";
    } catch (err) {
      if (err.name === "SecurityError" || err.name === "SyntaxError") {
        console.error(`${err.name}: ${err.message}`);
      } else {
        throw err;
      }
    }
  });
}
```

The HTML Standard tells browsers to remember declined registrations so the user is not prompted again, which means a repeated click after a refusal may show nothing; say so in the button's help text.

### Declaring the same handler in the manifest and reading the launched URL

Installed users get the association without a prompt once the manifest carries the entry. The route receives the whole link in the `song` query parameter, scheme included, and has to strip the prefix itself.

```json
{
  "name": "Player",
  "start_url": "/",
  "scope": "/",
  "protocol_handlers": [
    { "protocol": "web+music", "url": "/play?song=%s" }
  ]
}
```

```js
const launched = new URL(location.href).searchParams.get("song");

if (launched && launched.startsWith("web+music:")) {
  const trackId = launched.slice("web+music:".length); // "track/42"
  loadTrack(trackId);
} else {
  showLibrary(); // opened directly, not through a protocol link
}
```

Keep the route inside the manifest `scope`: an out-of-scope `url` is dropped from the processed manifest, and the install proceeds with no handler and no warning.

### Removing a handler the user no longer wants

`unregisterProtocolHandler()` takes the same two arguments and exists in Chromium only. Guard it separately from `registerProtocolHandler()`, because Firefox has the first method and not the second.

```js
function forgetHandler() {
  if (typeof navigator.unregisterProtocolHandler !== "function") {
    return false; // Firefox: removal is only available from the browser's own settings
  }
  navigator.unregisterProtocolHandler("web+music", "/play?song=%s");
  return true;
}
```

Unregistering through the method does not touch an association created by the manifest; that one is removed with the installed app.

## See also

- [Manifest protocol_handlers](/reference/manifest/protocol-handlers/), the member's own entry with its processing details
- [Manifest file_handlers](/reference/manifest/file-handlers/), the equivalent OS registration for file types
- [Handle files](/guides/file-handling/)
- [HTML Standard: registerProtocolHandler() method](https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator-registerprotocolhandler) (html.spec.whatwg.org)
- [HTML Standard: safelisted schemes](https://html.spec.whatwg.org/multipage/system-state.html#safelisted-scheme) (html.spec.whatwg.org)
- [Web App Manifest incubations: protocol_handlers member](https://wicg.github.io/manifest-incubations/#protocol_handlers-member) (wicg.github.io)
- [URL protocol handler registration for PWAs](https://developer.chrome.com/docs/web-platform/best-practices/url-protocol-handler) (developer.chrome.com)
- [Handle protocols in Progressive Web Apps](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps/how-to/handle-protocols) (learn.microsoft.com)