VAPID keys for web push, explained
VAPID is how a push service knows who is sending. It’s a key pair: the public half goes into the browser when someone subscribes, the private half signs every message your server sends.
- VAPID (RFC 8292) identifies your server to push services with an ECDSA P-256 key pair.
- The public key is passed to pushManager.subscribe(); the private key signs a short-lived JWT for each request.
- Subscriptions are bound to the public key — change keys and existing subscribers can’t be reached.
- Never commit or expose the private key; load it from environment or a secrets store.
What VAPID does
Voluntary Application Server Identification (RFC 8292) lets a push service verify that requests to a subscription come from the server that created it. When a browser subscribes with your public key as applicationServerKey, the push service ties that subscription to the key. Later, each send carries a JWT signed with the matching private key; the push service rejects anything else.
The signed token
Authorization: vapid t=<JWT>, k=<base64url public key>
JWT header : { "typ": "JWT", "alg": "ES256" }
JWT claims : {
"aud": "https://fcm.googleapis.com", // origin of the push endpoint
"exp": 1794560000, // no more than 24 hours ahead
"sub": "mailto:push@yoursite.com" // a contact for the push service
}Libraries build this for you from the endpoint URL and your keys (full protocol).
Generating a key pair
# Node.js (web-push package)
npx web-push generate-vapid-keys
# PHP (minishlink/web-push)
php -r 'require "vendor/autoload.php"; print_r(Minishlink\WebPush\VAPID::createVapidKeys());'
# OpenSSL (PEM files; convert to base64url for browsers)
openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem
openssl ec -in vapid_private.pem -pubout -out vapid_public.pemBrowsers expect the public key as an uncompressed P-256 point (65 bytes) encoded in base64url — the format the Node and PHP generators print.
Storing keys safely
- Keep the private key in environment variables or a secrets manager, never in your repository or front-end bundle.
- Back it up. Losing it is the same as losing every subscriber.
- Use one key pair per site or app so a leak doesn’t affect everything.
- Restrict who can read it on the server; the private key is a send-to-everyone credential.
Rotation and portability
Because subscriptions are bound to the public key, rotating keys means existing subscriptions stop working: browsers must resubscribe with the new key. This is also why switching push vendors usually means rebuilding the list — the old vendor holds the private key (switching providers, owning your list). Rotate only when you must, and plan a resubscribe on next visit.
VAPID errors you’ll actually see
| Response | Usually means |
|---|---|
| 403 from FCM (“credentials … do not correspond”) | Sending with a different key pair than the subscription was created with |
| 403 BadJwtToken (Apple) | Bad or expired JWT, or a missing/invalid sub claim |
| 401 (Mozilla) | Missing or malformed Authorization header |
| 400 | Malformed headers, often a key in the wrong encoding (base64 vs. base64url) |
The JWT’s exp must be no more than 24 hours ahead; libraries handle this, but clock drift on a server can still cause failures.
Choosing the subject (sub) claim
The sub claim tells push services how to contact you about problems: a mailto: address or an https: URL. Use a monitored, real address on your own domain. Placeholder values such as mailto:admin@localhost have been reported to fail with Apple’s push service, so don’t ship them to production.
One key pair or many?
One pair per application is the norm, even across several domains — the key identifies the sender, while subscriptions are per origin anyway. Separate pairs make sense when different teams or customers must never be able to send to each other’s subscribers; that’s how multi-tenant services isolate customers. Whatever you choose, losing a private key means losing the ability to reach every subscription created with it, so back it up like a database password.
Using Pusharoo instead
With Pusharoo you don’t generate or store VAPID keys yourself; the service manages them and your site only needs sw.js and the snippet. If you’re building your own stack for full control, the steps above are what you’ll own (client code).
Questions
Is the VAPID public key secret?
No. It’s embedded in your page so browsers can subscribe. Only the private key is secret.
What should the VAPID subject be?
A mailto: address or https: URL where the push service can contact you about your traffic.
Can I use the same VAPID keys for multiple sites?
Technically yes, but separate keys per site limit the impact of a leak and make migrations cleaner.
What VAPID subject should I use?
A mailto: address or https: URL where push services can contact you. Apple’s service rejects placeholder subjects, so use a real one.
Keep reading
The Web Push protocol, explained
RFC 8030 delivery headers, RFC 8291 encryption, RFC 8292 VAPID, status codes and payload size.
The Web Push API in JavaScript: a working example
Register a worker, ask on click, subscribe with VAPID, store the subscription and send — complete code.
Web push notifications with Node.js
Install web-push, generate keys, save subscriptions with Express, send with TTL and clean up dead endpoints.
Switching web push providers: a practical migration plan
What can and can’t be migrated, why VAPID keys matter, and a step-by-step plan to switch tools.
More in Developers: Service worker push events, done right · Web push notifications in PHP · Laravel browser push notifications (no Firebase) · Web push notifications in React and Next.js · Declarative Web Push, explained · Firebase web push vs. standard web push · Web push in Python with pywebpush · Django push notifications to the browser · Web push notifications in Ruby on Rails · Web push in Go with webpush-go · Angular push notifications with SwPush · Push notifications in Vue 3 and Nuxt · Push notifications in SvelteKit · All guides
Free for life. No limits.
Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.