Skip to content
Developers · 8 min read

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.

Updated By the Pusharoo team
In short
  • 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.pem

Browsers 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

ResponseUsually 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
400Malformed 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.

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.