Notification inbox

Web notification inbox

Add a notification bell with an unread badge and an inbox panel to a website — plain JS, React or Next.js.

The bell is the in-app history; web push wakes the browser. Set up web push first — the bell stays empty until the browser registers.

Add the bell

The widget is three files in native-notify/web/: native-notify-bell.js (plain JS), NativeNotifyBell.jsx (React and the headless hook) and nativeNotifyBell.css. Drop them into your site, or ask Agent Notify to add them in a pull request.

// app/site-header.tsx
"use client";

import NativeNotifyBell from "../native-notify/web/NativeNotifyBell";

export function SiteHeader() {
  return (
    <header>
      {/* your header… */}
      <NativeNotifyBell appId={123} webKey="nnweb_…" title="Notifications" />
    </header>
  );
}

Import the stylesheet once — in app/layout.tsx (App Router) or pages/_app.tsx (Pages Router):

import "../native-notify/web/nativeNotifyBell.css";

Build your own UI

The headless hook returns the same data and actions. It loads the unread count itself — call refresh() for the first page of entries:

"use client";

import { useEffect } from "react";
import { useNativeNotifyInbox } from "../native-notify/web/NativeNotifyBell";

export function InboxList() {
  const inbox = useNativeNotifyInbox({ appId: 123, webKey: "nnweb_…", take: 20 });
  const { refresh } = inbox;

  // Fetch page 1 — this re-runs once the per-browser device id has resolved.
  useEffect(() => { refresh(); }, [refresh]);

  if (inbox.error) return <p role="alert">{inbox.error}</p>;
  return (
    <ul>
      {inbox.entries.map((entry) => (
        <li key={entry.entryId}>
          <button type="button" onClick={() => inbox.markRead(entry)}>
            {entry.read ? "" : "• "}{entry.title}
          </button>
        </li>
      ))}
    </ul>
  );
}

It returns deviceId, entries, total, unreadCount, loading, error and the actions refresh, refreshUnread, markRead(entry), markAllRead, remove(entry), clearInbox and loadMore.

Choose the device

The bell shows one device's inbox — by default this browser's, whose id lives in localStorage under nn_web_device_id, the key the web push snippet registers under. To show another device, pass its id; plain JS resolves it with getDeviceId:

mountNativeNotifyBell({
  appId, webKey,
  getDeviceId: async () => (await fetch("/api/me/device-id").then((r) => r.json())).deviceId,
});

The web key reads only devices it registered, so a native device answers 403 web_key_scope.

Options

React takes these as props. Plain JS (mountNativeNotifyBell, createNativeNotifyBell) takes the same names as options.

OptionDefaultWhat it does
appId, webKey—Required. webKey is the publishable web key.
deviceIdper-browser keyWhose inbox to show. React takes the resolved id here.
getDeviceId—Plain JS only. async () => deviceId.
storageKey"nn_web_device_id"localStorage key of the default id.
apiBasehttps://app.nativenotify.comPoint the widget at your own proxy.
take20Rows per page, max 200.
title"Notifications"Panel header.
emptyText"You are all caught up."Empty state.
showCounttruefalse shows a plain dot.
maxCount99Badge cap, shown as 99+.
allowDeletetrueShow the delete button on each row.
pollMs30000Unread poll interval in milliseconds. 0 disables polling; use 10000 or more.
themelight and dark defaultsColor overrides, like { accent: "#2563eb", dot: "#ef4444", radius: "14px" }. Each key sets a CSS variable such as --nn-accent.
position"bottom-right"Floating corner: bottom-right, bottom-left, top-right or top-left.
mount<body>Plain JS only. Element or selector to mount into.
showLoadMoretruePlain JS only. A Load more button when more pages exist.
onNotificationPress, onNavigate, onUnreadChange, onError—Callbacks: every row click, a deep-link handler (url, entry), the unread count on change, and request errors (plain JS only).
className—React only. Extra class on the root element.

Put a url in the send's pushData:

{
  "title": "New service times",
  "message": "This Sunday: 9am and 11am.",
  "pushData": { "url": "/news/service-times" }
}

A click marks the row read, then opens the URL. Only same-origin paths and http(s) URLs open; anything else is ignored. Pass onNavigate to route client-side.

The inbox API

The widget calls the six inbox endpoints with the web key where an app token would go — in the URL for GETs, in the body for POSTs. Your side of the same data — every send with its read counts, and who is registered — is in the dashboard's Inbox and Audience tabs.

Plans

On Premium every inbox call answers 403 plan_upgrade_required, and the panel shows that message to your visitors. Sends still write entries, so they appear after an upgrade. See what each plan includes.

A browser that hasn't registered with the web key answers 403 web_key_scope; the widget shows an empty inbox. Every other code is in Mobile notification inbox.

The web key

The widget's credential is visible in your page, so it uses the publishable web key: it can register one browser device and read or mark that device's own inbox — never send, list groups or subscribers, or touch another device. Copy it from App Settings → App keys, or read it with a dashboard session or MCP token:

GET https://app.nativenotify.com/api/apps/<APP_ID>/web-key

To keep even the web key out of your page, proxy the six calls through your own API and set apiBase. See App keys & token rotation.