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 tokenPublishable web key
Looks like22 alphanumeric charactersnnweb_…
Can doEverything the app can: send to all users, read inboxes, manage groups, read analyticsRegister / 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 everyoneYes — that is its whole purpose
Where it livesYour server/configYour 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 browser PushSubscription) 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 403 with { "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

CallEndpoint
Register a web devicePOST /api/universal/device/register — { appId, appToken: <webKey>, platform: "web", deviceId, webPush }
Deregister itPOST /api/universal/device/deregister — { appId, appToken: <webKey>, deviceId }
List / unreadGET /api/universal/inbox/:appId/:webKey?deviceId=… · …/unread-count?deviceId=…
Mark / deletePOST /api/universal/inbox/read · read-all · delete · clear — { appId, appToken: <webKey>, deviceId, entryId? }
Public VAPID keyGET /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; 0 retires it at once). The response carries the new token and previousToken: { 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: false when nothing was waiting). After the window (or a retire) the old token answers 401 like 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 (a developer teammate gets 403 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.

Next