Vapid/WebPush Notifications
How Web Push Works
Section titled “How Web Push Works”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:
- Somebody visits your site and clicks whatever button you use to ask permission for notifications.
- 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. - You save that JSON into a
subscriptions.jsonfile. - Apprise reads the file, encrypts your message for that browser, signs it with your private key, and sends it to the
endpointthe 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.
Account Setup
Section titled “Account Setup”You need three things:
| Thing | What it is |
|---|---|
| A contact email | Apprise uses this as the VAPID sender identity. It is the first part after vapid://, such as you@example.com. |
| A private key | A 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.json | The browser subscriptions you collected, as described above. You must supply this yourself. |
Use the Same VAPID Key Everywhere
Section titled “Use the Same VAPID Key Everywhere”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.
Starting Without a VAPID Key
Section titled “Starting Without a VAPID Key”Create a P-256 private key and keep it private:
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pemchmod 600 private_key.pemMany 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:
python - <<'PY'from base64 import urlsafe_b64encodefrom 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())PYYour 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.
Setup Checklist
Section titled “Setup Checklist”- Create or select one VAPID key pair.
- Configure its public value as your website’s
applicationServerKey. - Ask each browser for permission and save its subscription JSON.
- Put the subscriptions into the named format shown below.
- Give Apprise the matching private key and subscription file.
- Send a test with
-vvso any setup problem is visible.
Where to Put Your Files
Section titled “Where to Put Your Files”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.
-
Find the directory for your URL:
Terminal window apprise storage list -
Copy your subscriptions into the directory it names:
Terminal window cp subscriptions.json ~/.local/share/apprise/cache/<id>/ -
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.
Best when the files already live somewhere specific and you would rather leave them there.
Name the files on the URL. Any readable path works:
apprise -vv -t "Title" -b "Message content" \ "vapid://you@example.com/phone?keyfile=/etc/apprise/private_key.pem&subfile=/etc/apprise/subscriptions.json"Or, more comfortably, in a configuration file:
urls: - vapid://you@example.com/phone: keyfile: /etc/apprise/private_key.pem subfile: /etc/apprise/subscriptions.jsonIf Apprise can write to the file, it also tidies out subscriptions that stop working. If the file is read-only, it is left untouched. Either way it keeps working.
Best when you cannot reach the filesystem at all, which is the usual situation behind the Apprise API.
Point at a web address instead of a path. Apprise fetches it when it sends, and a username and password are supported:
urls: - vapid://you@example.com/phone: keyfile: https://user:pass123@example.com/private_key.pem subfile: https://user:pass123@example.com/subscriptions.jsonYou keep the file wherever is convenient for you and edit it there; Apprise only reads it.
Syntax
Section titled “Syntax”Valid syntax is as follows:
vapid://you@example.com/vapid://you@example.com/phonevapid://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.
Parameter Breakdown
Section titled “Parameter Breakdown”| Variable | Required | Description |
|---|---|---|
| keyfile | No | The 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. |
| subfile | No | A 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. |
| mode | No | A 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. |
| ttl | No | How 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. |
About the mode Setting
Section titled “About the mode Setting”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.
| Mode | Browser family |
|---|---|
| chrome | Google Chrome |
| firefox | Mozilla Firefox |
| edge | Microsoft Edge |
| opera | Opera |
| apple | Safari and other Apple browsers |
Global Parameters
Section titled “Global Parameters”| Variable | Description |
|---|---|
| overflow | Controls 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. |
| format | This 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. |
| verify | External 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. |
| redirect | By 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. |
| cto | This 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. |
| rto | This 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. |
| emojis | Enable 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. |
| tz | Identify 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. |
| retry | The 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. |
| wait | The 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. |
| optional | When 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. |
subscriptions.json Setup
Section titled “subscriptions.json Setup”VAPID requires a subscriptions.json file. Apprise supports two formats:
-
standalone; in the below example, the target would be
abc123{"endpoint": "https://fcm.googleapis.com/fcm/send/abc123","keys": {"p256dh": "BNcW4oA7zq5H9TKIrA3XfKclN2fX9P_7NR...","auth": "k9Xzm43nBGo="}} -
multiple target support; in the below example, 2 targets are created called
name1andname2{"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 plainhttp) - has no host name, or a host name that is not valid
- contains a username or password
Subscriptions That Stop Working
Section titled “Subscriptions That Stop Working”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.
Private Key (PEM) Setup
Section titled “Private Key (PEM) Setup”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.
Persistent Storage Tips
Section titled “Persistent Storage Tips”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.pemThe following command will list all of the persistent storage locations associated with your configuration:
apprise storage listSimply locate the ID associated with the Vapid account you wish to update; consider that the directory ID’s can be found as:
- Microsoft Windows:
%APPDATA%/Apprise/cache - Linux:
~/.local/share/apprise/cache
For more details on this; see here.
Moving Your Setup to Another Server
Section titled “Moving Your Setup to Another Server”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:
- Run
apprise storage liston the old server and note the ID for your Vapid URL. - Copy that directory’s
subscriptions.jsonandprivate_key.peminto the same-named directory under the new server’s storage path. - 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.
Apprise URL Construction
Section titled “Apprise URL Construction”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.jsonBoth 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.jsonIf 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:Examples
Section titled “Examples”Notify the subscription named after your own contact address:
apprise -vv -t "Title" -b "Message content" \ vapid://you@example.comNotify two named targets from your subscriptions.json:
apprise -vv -t "Title" -b "Message content" \ vapid://you@example.com/laptop/phonePoint at your own files instead of the persistent storage copies:
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:
apprise -vv -t "Title" -b "Message content" \ "vapid://you@example.com/phone?ttl=30"Troubleshooting
Section titled “Troubleshooting”Run tests with -vv; VAPID failures are usually configuration problems.
| Symptom | What to Check |
|---|---|
Vapid could not load subscriptions | Confirm subfile, persistent storage, JSON syntax, and file permissions. |
| Target not found | Match the URL target to a top-level name in subscriptions.json. |
401 or 403 response | The private key probably does not match the public applicationServerKey used to create the subscription. |
404 or 410 response | The browser subscription expired. Ask that browser to subscribe again and save its new JSON. |
| Endpoint rejected | Copy the browser-provided HTTPS endpoint exactly; do not add credentials or edit its host. |
| Works locally but not in a container | Mount both files, use persistent storage, or provide paths that exist inside the container. |
Questions or Feedback?
Technical Issues
Having trouble with the code? Open an issue on GitHub: