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.