App Keys & Token Rotation
The publishable web key (page-source-safe: register/deregister one browser device and read its own inbox) and app-token rotation with an owner-chosen grace window — endpoints, the dashboard, and the MCP tools.
Every Native Notify app has two credentials with very different jobs:
| App token | Publishable web key | |
|---|---|---|
| Looks like | 22 alphanumeric characters | nnweb_… |
| Can do | Everything the app can: send to all users, read inboxes, manage groups, read analytics | Register / deregister one browser device and read or mark that one device's own inbox (plus read the app's public VAPID key) |
| Can NOT do | — | Send, list groups or subscribers, read another device's data, touch classic/account routes |
| Safe in page source? | No — it can send to everyone | Yes — that is its whole purpose |
| Where it lives | Your server/config | Your website's page source (web push + notification-bell snippets) |
Both are accepted in the same place: wherever an endpoint documents appToken (the :appToken URL segment on the universal GETs, the appToken body field on the POSTs). The service tells them apart by the nnweb_ prefix, and a web-key request is refused (403 web_key_not_allowed) on anything outside the list above. Nothing changed for existing integrations: the app token keeps working everywhere.
The publishable web key
Why it exists: web snippets run in the browser, and page source is public. An app token there would let anyone send push notifications to every user of that app. The web key is the credential those snippets embed instead — see the Web Push Recipe and the Notification Bell & Inbox (Web):
- it may register a
platform: "web"device (a browserPushSubscription) and deregister it again; - it may list, mark read, delete and clear that registered device's own inbox entries;
- it may read the app's public VAPID key (public material a browser needs to subscribe);
- everything else answers
403with{ "error": { "code": "web_key_not_allowed", … } }.
The scope is not just documented, it is enforced twice: the auth layer only lets a web key reach that short list of routes, and the universal service only lets it touch devices it registered itself — a device registered with the app token (or on iOS/Android) is out of reach even if its deviceId is known (403 web_key_scope).
Read (or create) the key
Like the VAPID keypair, the key is created server-side the first time you read it — nothing to paste anywhere:
GET https://app.nativenotify.com/api/agent/apps/<APP_ID>/web-key
Dashboard session or MCP token (an app token is refused on /api/agent/*). The answer:
{
"ok": true,
"app": { "id": 123, "name": "Corner Bakery" },
"webKey": "nnweb_1f1c…",
"webKeyCreatedAt": "2026-09-29T12:00:00.000Z",
"scope": { "canRegister": true, "canReadOwnDeviceInbox": true, "canSend": false },
"webPush": { "tokenType": "web", "registerPath": "/api/universal/device/register", "maxPayloadBytes": 3993 }
}
On the dashboard: App Settings → App keys → Publishable web key (copy, or regenerate). As an agent: the MCP tool get_app_web_key.
What a web key may call
| Call | Endpoint |
|---|---|
| Register a web device | POST /api/universal/device/register — { appId, appToken: <webKey>, platform: "web", deviceId, webPush } |
| Deregister it | POST /api/universal/device/deregister — { appId, appToken: <webKey>, deviceId } |
| List / unread | GET /api/universal/inbox/:appId/:webKey?deviceId=… · …/unread-count?deviceId=… |
| Mark / delete | POST /api/universal/inbox/read · read-all · delete · clear — { appId, appToken: <webKey>, deviceId, entryId? } |
| Public VAPID key | GET /api/universal/web-push/keys/:appId/:webKey |
Everything else — sends, groups, audiences, health, environments, analytics, classic endpoints, account routes — is refused for a web key. A request on another app never matches at all (401, like a wrong token).
Rotate the web key
POST https://app.nativenotify.com/api/agent/apps/<APP_ID>/web-key/rotate { "confirm": true }
→ { "ok": true, "rotated": true, "previousWebKey": "nnweb_…", "webKey": "nnweb_…", "webKeyCreatedAt": … }
The old key stops authenticating immediately — there is no grace window for it (it is publishable material, and a site ships the new key in one deploy). Owner/admin only; confirmation_required without "confirm": true. MCP tool: rotate_app_web_key.
App-token rotation (opt-in)
The app token is a powerful credential; rotating it is how you recover after it leaks — without breaking your own integration, because you choose how long the old token keeps working.
GET https://app.nativenotify.com/api/agent/apps/<APP_ID>/app-token
POST https://app.nativenotify.com/api/agent/apps/<APP_ID>/app-token/rotate { "confirm": true, "graceDays": 7 }
POST https://app.nativenotify.com/api/agent/apps/<APP_ID>/app-token/retire-previous { "confirm": true }
- Rotate issues a brand-new 22-character app token, valid immediately, and keeps the old one valid for
graceDays(0–90, default 7;0retires it at once). The response carries the new token andpreviousToken: { activeUntil, graceDays }— the previous token's value is never echoed back. - Get returns the current token plus the grace state:
{ appToken, rotation: { previousTokenActive, previousTokenExpiresAt } }. - Retire ends the grace window early. Idempotent (
retired: falsewhen nothing was waiting). After the window (or a retire) the old token answers401like any unknown pair. - A second rotation replaces the grace: only one previous token exists, so the older one stops at once.
- Every mutation is audit-logged (
agent_events) and owner/admin-only (adeveloperteammate gets403 admin_only). MCP tools:get_app_token_status,rotate_app_token,retire_previous_app_token. Dashboard: App Settings → App keys → App token (with the grace-period picker).
What rotation does to your data
Rows that reference the token — registered devices, subscribers, classic inboxes, scheduled pushes — follow the app to the new token automatically (the database cascades the update), so token-filtered queries behave exactly as before. The one thing rotation cannot do for you is update the places that authenticate with the old token: your app binary, your server config, your .env. That is what the grace window is for — update them before it ends.
Rotating the app token does not touch the publishable web key, your devices, or your inbox history.
Which credential should my web app use?:
The web key, always: it is what the web push and bell snippets embed, and it cannot do anything dangerous. Keep the app token on your server (sending, scheduling, analytics, group management) and for your native apps' build configuration.
Agent-first
Everything on this page is doable by an agent through the MCP server: get_app_web_key, rotate_app_web_key, get_app_token_status, rotate_app_token and retire_previous_app_token. Rotation is live and cannot be recalled — the tool descriptions tell agents to confirm with the user first.
- Web Push Recipe — the browser flow that uses the web key.
- Notification Bell & Inbox (Web) — the widget that uses the web key.
- Device Registration — the full registration contract.
- MCP server — the tools an agent uses.