Custom data

Carry links, images, categories and ids in a notification's data object and use them in your own UI.

Besides its localized text, a notification can carry a free-form data object: any JSON your app needs to render or handle it. It is shared by every locale and delivered unchanged in each feed item.

{
  "audience": { "type": "users", "userIds": ["user_123"] },
  "contents": { "en": { "title": "Ada commented on “Q3 roadmap”", "body": "Looks great, ship it!" } },
  "data": {
    "url": "/docs/q3-roadmap#comment-881",
    "imageUrl": "https://cdn.example.com/avatars/ada.png",
    "category": "comments",
    "documentId": "doc_42"
  }
}

There are no reserved keys: pick names and use them consistently. Typical uses:

  • a link to open on click (url);
  • a picture for the row, such as an avatar or a product thumbnail (imageUrl);
  • a grouping key for icons, colors or tabs (category);
  • ids your UI needs (documentId, orderId);
  • mobile navigation targets ({ "screen": "Order", "params": { "id": "o_1" } });
  • how to present the notification when it arrives ({ "display": "popup" }, see Building your UI);
  • values for templates: {{data.documentId}}.

Limits

data is limited to 8 KB of serialized JSON (larger payloads get 400 bad_request). Keep it small anyway: it is part of every feed response that contains the item. It is visible to every user who receives the notification, so don't include secrets or other users' private data.

data is delivered as you sent it: the API doesn't check URLs in it, and templates aren't rendered in it. Check links before you open them (see below).

Using it in your app

Every feed item has data (null when not set). Its values are unknown in TypeScript, so narrow them before use:

import { type FeedItem, isSafeUrl } from "@notificationstream/react";

function dataString(item: FeedItem, key: string): string | null {
  const value = item.data?.[key];
  return typeof value === "string" ? value : null;
}

export function itemUrl(item: FeedItem): string | null {
  const url = dataString(item, "url");
  return url && isSafeUrl(url) ? url : null;
}

function NotificationRow({ item }: { item: FeedItem }) {
  const image = dataString(item, "imageUrl");
  const category = dataString(item, "category");
  return (
    <div className={category ? `notification notification-${category}` : "notification"}>
      {image?.startsWith("https://") && <img src={image} alt="" width={32} height={32} />}
      <p>{item.title}</p>
    </div>
  );
}

isSafeUrl passes http(s), mailto, relative paths and # anchors, so a javascript: URL never reaches location.assign or an href. For the click itself (track the click, mark it read, then navigate), see Handling clicks.

On React Native, Linking.openURL only opens absolute URLs. Send app routes as navigation targets instead and hand them to your navigator:

const screen = item.data?.screen;
if (typeof screen === "string") navigation.navigate(screen);

Grouping by category

The feed has no server-side category filter: GET /v1/client/feed filters only by status. To group or filter the items you've loaded, filter them in your UI:

const { items } = useNotifications();
const billing = items.filter((item) => item.data?.category === "billing");

This filters the pages loaded so far. unreadCount and unseenCount always cover the whole feed (its newest 100 notifications), not one category.

Notifications sent before November 2026

Notifications used to have dedicated category, actionUrl and imageUrl fields. Those of existing notifications were moved into data under the same names (data.category, data.actionUrl, data.imageUrl), unless data already had that key. See the changelog.