Skip to content

Vapid/WebPush Notifications

Overview

Web Push is how a website sends a notification to a browser that has already visited it. If you have never set one up before, the important thing to understand is that you do not choose where the notification goes; the browser tells you.

The flow looks like this:

  1. Somebody visits your site and clicks whatever button you use to ask permission for notifications.
  2. Their browser hands your site back a small block of JSON called a subscription. It contains an endpoint (a long web address belonging to Google, Mozilla, Apple, or whoever makes that browser) plus two keys used to encrypt messages so only that browser can read them.
  3. You save that JSON into a subscriptions.json file.
  4. Apprise reads the file, encrypts your message for that browser, signs it with your private key, and sends it to the endpoint the browser gave you.

The endpoint is unique to one browser on one device. There is nothing in your Apprise URL that decides where a message is delivered; that comes entirely out of the subscription. This is why you need the subscriptions.json file before Apprise can notify anybody.

You need three things:

ThingWhat it is
A contact emailApprise uses this as the VAPID sender identity. It is the first part after vapid://, such as you@example.com.
A private keyA private_key.pem file used to sign each message so the push service can confirm it really came from you. Apprise can create this for you.
subscriptions.jsonThe browser subscriptions you collected, as described above. You must supply this yourself.

Choose the VAPID key pair before browsers subscribe. Your website gives the public key to the browser as applicationServerKey; Apprise needs the matching private_key.pem. A push service rejects messages signed by a different key.

The p256dh and auth values in subscriptions.json are separate browser encryption keys. They are not your VAPID public key.

If your website already creates Web Push subscriptions, reuse its existing VAPID private key. Changing keys means every browser must subscribe again.

Create a P-256 private key and keep it private:

Terminal window
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem
chmod 600 private_key.pem

Many web frameworks can derive the public value for you. If yours cannot, run this beside private_key.pem and copy the printed value into your website’s applicationServerKey setting:

Terminal window
python - <<'PY'
from base64 import urlsafe_b64encode
from cryptography.hazmat.primitives import serialization
with open("private_key.pem", "rb") as stream:
key = serialization.load_pem_private_key(stream.read(), password=None)
raw = key.public_key().public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.UncompressedPoint,
)
print(urlsafe_b64encode(raw).rstrip(b"=").decode())
PY

Your website then asks the browser to subscribe with that public value and saves subscription.toJSON(). The result contains the endpoint, p256dh, and auth values Apprise needs.

  1. Create or select one VAPID key pair.
  2. Configure its public value as your website’s applicationServerKey.
  3. Ask each browser for permission and save its subscription JSON.
  4. Put the subscriptions into the named format shown below.
  5. Give Apprise the matching private key and subscription file.
  6. Send a test with -vv so any setup problem is visible.

Apprise needs to find your subscriptions.json (and a private_key.pem). There are three ways to arrange this; pick the one that matches how you run Apprise.

Best when you run Apprise yourself and can reach the filesystem.

Leave subfile and keyfile off the URL entirely. Apprise gives every URL its own storage directory and looks for the files there. It can generate a key pair, but browsers must subscribe with that pair’s public key, so create or locate the key before collecting subscriptions.

With automatic PEM generation enabled (the default), Apprise creates a missing subscriptions.json with one example entry. Replace that placeholder with your own subscription before sending notifications.

  1. Find the directory for your URL:

    Terminal window
    apprise storage list
  2. Copy your subscriptions into the directory it names:

    Terminal window
    cp subscriptions.json ~/.local/share/apprise/cache/<id>/
  3. Use a clean URL with nothing pointing anywhere:

    Terminal window
    apprise -vv -t "Title" -b "Message content" \
    vapid://you@example.com/phone

This is the tidiest option. The URL carries no file paths, so it can be copied between machines and shared in a configuration file. See Moving Your Setup to Another Server.

Valid syntax is as follows:

  • vapid://you@example.com/
  • vapid://you@example.com/phone
  • vapid://you@example.com/laptop/phone/

A target is the name you gave a subscription inside subscriptions.json. If you omit targets, Apprise looks for one named after your contact email.

VariableRequiredDescription
keyfileNoThe P-256 VAPID private key in PEM format. When omitted, Apprise uses the key in this URL’s persistent-storage directory and can create one there. Its public half must match the key used by the browser.
subfileNoA subscriptions.json file identifying the configuration you wish to reference. This can be a local path or a remote location such as https://user:pass@example.com/subscriptions.json. When left out, the file in this URL’s persistent storage directory is used.
modeNoA compatibility label retained in the URL (default chrome). It does not select the destination. Usually leave it unchanged. Possible values are chrome, firefox, edge, opera, and apple.
ttlNoHow long, in seconds, the push service may retain a message for an offline device (default 0, maximum 60). 0 means deliver immediately or discard it.

mode is retained for compatibility and forms part of the URL’s storage identity. It does not choose the push service; delivery always uses the endpoint inside each subscription. Most users should leave the default unchanged. Changing it can select a different persistent-storage directory.

ModeBrowser family
chromeGoogle Chrome
firefoxMozilla Firefox
edgeMicrosoft Edge
operaOpera
appleSafari and other Apple browsers
VariableDescription
overflowControls messages that exceed a service’s documented limit. The default is upstream.
👉 upstream: Send one message without splitting or truncating it for that limit.
👉 truncate: Keep the portion that fits and discard the rest.
👉 split: Prefer a readable break, fall back to a hard boundary, and send every part in order.
Splitting undeclared or structured content is best effort. Use upstream when the body must remain one intact document.
formatThis parameter can be set to either text, html, or markdown. Some services support the ability to post content by several different means. The default of this varies (it can be one of the 3 mentioned at any time depending on which service you choose). You can optionally force this setting to stray from the defaults if you wish. If the service doesn’t support different types of transmission formats, then this field is ignored.
verifyExternal requests made to secure locations (such as through the use of https) will have certificates associated with them. By default, Apprise will verify that these certificates are valid; if they are not then no notification will be sent to the source. In some occasions, a user might not have a certificate authority to verify the key against or they trust the source; in this case you will want to set this flag to no. By default it is set to yes.
redirectBy default, Apprise will follow HTTP redirects (3xx responses) issued by the remote server, matching the behaviour of the underlying requests library. If you want to prevent custom headers and credentials from being forwarded to destinations that differ from the original URL, set this to no. By default it is set to yes.
ctoThis stands for Socket Connect Timeout. This is the number of seconds Requests will wait for your client to establish a connection to a remote machine (corresponding to the connect()) call on the socket. The default value is 4.0 seconds.
rtoThis stands for Socket Read Timeout. This is the number of seconds the client will wait for the server to send a response. The default value is 4.0 seconds.
emojisEnable Emoji support (such as providing :+1: would translate to 👍). By default this is set to no.
Note: Depending on server side settings, the administrator has the power to disable emoji support at a global level; but default this is not the case.
tzIdentify the IANA Time Zone Database you wish to operate as. By default this is detected based on the configuration the server hosting Apprise is running on. You can set this to things like America/Toronto, or any other properly formated Timezone describing your area.
retryThe number of additional delivery attempts to make after the first failure before giving up. Accepts an integer in the range 0 to 10. The default is 0 (no retries — a single attempt is made). When combined with wait, Apprise pauses the specified number of seconds between each attempt.
waitThe number of seconds to pause between retry attempts. Accepts a decimal value in the range 0.0 to 20.0; integer values are promoted to float automatically. The default is 0.5. This value is only meaningful when retry is greater than zero — a service with retry=0 makes exactly one attempt regardless of the wait value.
optionalWhen set to yes, a delivery failure for this service is silently absorbed. The overall notify() call still evaluates as true even if this endpoint was unreachable, provided that every required (non-optional) service in the same batch succeeded. Setting this flag does not skip delivery or bypass retry logic — all configured retry attempts are still made before the failure is absorbed. By default this is set to no, meaning every failure is propagated to the caller.

VAPID requires a subscriptions.json file. Apprise supports two formats:

  1. standalone; in the below example, the target would be abc123

    {
    "endpoint": "https://fcm.googleapis.com/fcm/send/abc123",
    "keys": {
    "p256dh": "BNcW4oA7zq5H9TKIrA3XfKclN2fX9P_7NR...",
    "auth": "k9Xzm43nBGo="
    }
    }
  2. multiple target support; in the below example, 2 targets are created called name1 and name2

    {
    "name1": {
    "endpoint": "https://fcm.googleapis.com/fcm/send/...",
    "keys": {
    "p256dh": "BNcW4oA7zq5H9TKIrA3XfKclN2fX9P_7NR...",
    "auth": "k9Xzm43nBGo="
    }
    },
    "name2": {
    "endpoint": "https://web.push.apple.com/...",
    "keys": {
    "p256dh": "BNcW4oA7zq5H9TKIrA3XfKclN2fX9P_7NR...",
    "auth": "k9Xzm43nBGo="
    }
    }
    }

Targets let you notify one or more endpoints. Names are case-insensitive, and subscriptions from different browsers can share one file.

Use the named, multiple-target format for normal installations. It is easier to manage and lets Apprise remove individual expired entries. A standalone file has no entry name to remove, so replace it manually when that subscription expires.

Copy each endpoint exactly as the browser gave it to you. Apprise checks each one before using it and skips any subscription where the endpoint:

  • does not start with https:// (Web Push does not run over plain http)
  • has no host name, or a host name that is not valid
  • contains a username or password

People uninstall apps, clear their browser data, and replace phones. When that happens the push service tells Apprise the subscription no longer exists, and there is no point ever trying that endpoint again. What Apprise does next depends on whether it is allowed to change your file:

  • The file is writable. The dead entry is taken out of it, so the file stays tidy as devices come and go. Only that one entry is removed; everything else is left exactly as it was found, including any entry Apprise could not read. If you have a typo somewhere in the file, it stays there for you to fix rather than being quietly deleted.
  • The file is read-only, or it is remote. Apprise leaves it alone and remembers the endpoint in persistent storage for 30 days. A later send skips that endpoint; after 30 days it may be checked again. Without persistent storage, this memory does not survive a restart.

When a local file is rewritten, symlinks are followed and Apprise attempts to preserve permissions and ownership. Replacement is atomic, so readers do not see half-written JSON. Simultaneous writers are not locked; avoid having multiple processes edit the same file at once.

Apprise signs messages with private_key.pem. This must match the public applicationServerKey used when the browser subscribed. Put it in persistent storage, or select a local or remote file with keyfile. See Use the Same VAPID Key Everywhere before creating subscriptions.

When persistent storage is turned on, each Apprise URL gets its own directory named after an ID worked out from the URL itself. Your Vapid files live in there:

<storage>/<id>/subscriptions.json
<storage>/<id>/private_key.pem

The following command will list all of the persistent storage locations associated with your configuration:

Terminal window
apprise storage list

Simply locate the ID associated with the Vapid account you wish to update; consider that the directory ID’s can be found as:

  1. Microsoft Windows: %APPDATA%/Apprise/cache
  2. Linux: ~/.local/share/apprise/cache

For more details on this; see here.

The storage ID is worked out from your Apprise URL, not from anything specific to one machine, so the same URL lands on the same ID everywhere. Moving a working setup to another Apprise install is therefore a copy job:

  1. Run apprise storage list on the old server and note the ID for your Vapid URL.
  2. Copy that directory’s subscriptions.json and private_key.pem into the same-named directory under the new server’s storage path.
  3. Use the same Apprise URL on the new server.

Your URL stays clean and carries no file paths, so it is safe to keep in a shared configuration file used by more than one machine.

For repeatable installations, keep the URL and file locations in an Apprise YAML configuration:

urls:
- vapid://you@example.com/phone:
keyfile: /path/to/private_key.pem
subfile: /path/to/subscriptions.json

Both the keyfile and the subfile can also be hosted elsewhere, including behind a username and password. This is how you would run Vapid under the Apprise API, where you have no easy way to place files on the server:

urls:
- vapid://you@example.com/phone:
keyfile: https://user:pass123@example.com/private_key.pem
subfile: https://user:pass123@example.com/subscriptions.json

If you are using persistent storage, you can leave both out entirely and let Apprise use the files in this URL’s own storage directory:

urls:
- vapid://you@example.com:

Notify the subscription named after your own contact address:

Terminal window
apprise -vv -t "Title" -b "Message content" \
vapid://you@example.com

Notify two named targets from your subscriptions.json:

Terminal window
apprise -vv -t "Title" -b "Message content" \
vapid://you@example.com/laptop/phone

Point at your own files instead of the persistent storage copies:

Terminal window
apprise -vv -t "Title" -b "Message content" \
"vapid://you@example.com/phone?keyfile=/etc/apprise/private_key.pem&subfile=/etc/apprise/subscriptions.json"

Ask the push service to hold a message for up to 30 seconds if the device is offline:

Terminal window
apprise -vv -t "Title" -b "Message content" \
"vapid://you@example.com/phone?ttl=30"

Run tests with -vv; VAPID failures are usually configuration problems.

SymptomWhat to Check
Vapid could not load subscriptionsConfirm subfile, persistent storage, JSON syntax, and file permissions.
Target not foundMatch the URL target to a top-level name in subscriptions.json.
401 or 403 responseThe private key probably does not match the public applicationServerKey used to create the subscription.
404 or 410 responseThe browser subscription expired. Ask that browser to subscribe again and save its new JSON.
Endpoint rejectedCopy the browser-provided HTTPS endpoint exactly; do not add credentials or edit its host.
Works locally but not in a containerMount both files, use persistent storage, or provide paths that exist inside the container.
Questions or Feedback?

Documentation

Notice a typo or an error?

Technical Issues

Having trouble with the code? Open an issue on GitHub:

Made with love from Canada