App Inbox

App Inbox delivers messages from the Zeta Marketing Platform (ZMP) to a persistent, queryable store inside your app — similar to a notification center. Your app owns the view layer; the SDK provides the data.

Available since: React Native SDK 1.0.0.

App Inbox allows you to build a custom, persistent notification center inside your application. Since the SDK provides only the data layer, you have complete design freedom over list rows, detail screens, and unread badges. This guide will walk you through the process of integrating App Inbox.

For campaign setup and the message lifecycle in ZMP, see User Guide: App Inbox.

On this page


Understand how App Inbox works

Before writing code, it is helpful to understand the core mechanics of App Inbox:

  • Message Statuses: Each message has one of three statuses persisted locally: UNREAD, READ, or DELETED. New messages arrive as UNREAD. You should mark a message as READ when the user views it, and DELETED when they dismiss it.
  • Active Message Limit: The local store keeps at most 200 active (non-deleted, non-expired) messages, while the remote backup contains the most recent 50 messages. When a sync would exceed the local limit, the oldest active messages are automatically evicted.
  • Status Persistence: Status updates are maintained locally. If the local database is cleared (such as on opt-out or account switch), the next fetch will repopulate the store from the server, and all messages will return as UNREAD.

Important: ZetaClient.inbox is not available until ZetaClient.initialize() completes.

Wireframe inbox app UI mockup

An example of a custom inbox UI you build on top of the SDK's data layer — the SDK supplies the messages and their state; the presentation is entirely yours.


Access the inbox manager

The App Inbox APIs are available through ZetaClient.inbox, which implements the ZTInboxManagable interface:

import ZetaClient from 'zetakit_reactnative';

const inbox = ZetaClient.inbox;

Fetch messages from the server

To sync the latest messages from the server into the local database, call fetchMessages(). This method is Promise-based and returns the full list of non-deleted, non-expired messages:

try {
  const messages = await ZetaClient.inbox.fetchMessages();
  updateUI(messages);
} catch (error) {
  console.error('Failed to fetch inbox messages:', error);
}

Query the cached local store

Once messages are fetched, you can query the cached local database instantly without performing a network round-trip:

// Retrieve unread messages
const unreadMessages = await ZetaClient.inbox.getUnreadMessages();

// Retrieve read messages
const readMessages = await ZetaClient.inbox.getReadMessages();

// Retrieve total message count
const totalCount = await ZetaClient.inbox.getMessageCount();

// Retrieve unread message count (useful for UI badges)
const unreadCount = await ZetaClient.inbox.getUnreadMessageCount();

// Retrieve read message count
const readCount = await ZetaClient.inbox.getReadMessageCount();

// Fetch a single message by ID
const message = await ZetaClient.inbox.getMessage('MSG-001');

Understand the ZTAppInboxMessage model

A ZTAppInboxMessage object represents a single inbox entry and contains the following properties:

PropertyTypeDescription
messageIdstringUnique identifier. Use this when calling any inbox update method.
titlestringMessage headline.
bodystringMessage body text.
mediaUrlstring?Optional image or media URL associated with the message.
statusZTAppInboxMessageStatusCurrent UNREAD, READ, or DELETED state (see below).
expirationTimestampnumber?Unix timestamp (ms) after which the message should not be shown.
actionListZTAppInboxAction[]?CTA buttons attached to the message.
templateIdstringTemplate identifier used to decide the UI rendering style.
additionalDataRecord<string, string>?Arbitrary key-value pairs configured in the ZMP campaign.

The status field is a ZTAppInboxMessageStatus enum with one of three values:

ValueMeaning
UNREADDefault state for a freshly received message; the user has not opened or interacted with it yet.
READThe user has viewed the message, or your app has acknowledged it programmatically.
DELETEDThe user (or your app) has dismissed the message. Deleted messages are excluded from all query results but kept in the local store so the row is not re-inserted on the next sync.

Checking expiration manually

You can filter out expired messages in your UI using the expirationTimestamp:

if (message.expirationTimestamp) {
  const expiresAt = new Date(message.expirationTimestamp);
  if (expiresAt < new Date()) {
    // Message has expired; skip rendering
  }
}

Update message status

Keep the local database in sync with user interactions by updating message statuses.

Mark as read

Mark a message as read when the user taps on it to view details:

const updatedMessage = await ZetaClient.inbox.markMessageAsRead('MSG-001');

// Mark multiple messages as read
const ids = messages.map((m) => m.messageId);
const updatedList = await ZetaClient.inbox.markAllMessagesAsRead(ids);

Mark as deleted

Mark a message as deleted when the user swipes to dismiss it:

const deletedMessage = await ZetaClient.inbox.markMessageAsDeleted('MSG-001');

// Mark multiple messages as deleted
const updatedList = await ZetaClient.inbox.markAllMessagesAsDeleted(ids);

Handle Call-to-Action (CTA) buttons

Messages can include one or more CTA buttons represented by ZTAppInboxAction objects. Each action contains:

PropertyTypeDescription
textstringButton label shown to the user (e.g., "Shop Now").
typestringAction type identifier from the campaign template.
valuestringPrimary action value; pass this as the actionValue parameter to onMessageClicked().
linkstringURL associated with the action; may be a deep link or web URL.
actionTypestring?Hint for how to open link. Common values: webBrowser (in-app browser), externalBrowser (system browser).

Implementing CTA click tracking and routing

When a user taps a CTA button, call onMessageClicked() to record the click and trigger server-side automation, then route the user:

import { Linking } from 'react-native';

async function handleCTA(action: ZTAppInboxAction, message: ZTAppInboxMessage) {
  const actionValue = action.value || action.link;

  // Track the click event in the SDK
  await ZetaClient.inbox.onMessageClicked(message.messageId, actionValue);

  // Perform custom routing based on the link
  if (action.link) {
    if (action.actionType === 'externalBrowser') {
      Linking.openURL(action.link);
    } else {
      // Open in an in-app browser or navigate to a deep-linked screen
      Linking.openURL(action.link);
    }
  }
}

Render the inbox with FlatList

The snippets above are the building blocks. Here they come together in a React Native inbox screen that fetches on mount, renders rows with a FlatList, marks a message read on tap, deletes on a long press, and routes CTA taps through Linking.openURL:

import React, { useEffect, useState, useCallback } from 'react';
import { FlatList, Text, TouchableOpacity, View, Linking } from 'react-native';
import ZetaClient from 'zetakit_reactnative';
import type { ZTAppInboxMessage } from 'zetakit_reactnative';

export function InboxScreen() {
  const [messages, setMessages] = useState<ZTAppInboxMessage[]>([]);

  const refresh = useCallback(async () => {
    try {
      const list = await ZetaClient.inbox.fetchMessages();
      setMessages(list);
    } catch (error) {
      console.error('Failed to load inbox:', error);
    }
  }, []);

  useEffect(() => {
    refresh();
  }, [refresh]);

  const openMessage = async (message: ZTAppInboxMessage) => {
    await ZetaClient.inbox.markMessageAsRead(message.messageId);
    const cta = message.actionList?.[0];
    if (cta?.link) {
      await ZetaClient.inbox.onMessageClicked(message.messageId, cta.value || cta.link);
      Linking.openURL(cta.link);
    }
    refresh();
  };

  const deleteMessage = async (message: ZTAppInboxMessage) => {
    await ZetaClient.inbox.markMessageAsDeleted(message.messageId);
    refresh();
  };

  return (
    <FlatList
      data={messages}
      keyExtractor={(item) => item.messageId}
      onRefresh={refresh}
      refreshing={false}
      renderItem={({ item }) => (
        <TouchableOpacity
          onPress={() => openMessage(item)}
          onLongPress={() => deleteMessage(item)}
        >
          <View style={{ padding: 16, opacity: item.status === 'READ' ? 0.5 : 1 }}>
            <Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
            <Text numberOfLines={2}>{item.body}</Text>
          </View>
        </TouchableOpacity>
      )}
    />
  );
}

Clear the inbox on logout

To protect user data and ensure a clean state, always clear the local inbox database when a user logs out or switches accounts:

await ZetaClient.inbox.clearAll();

Next

Now that you have integrated App Inbox, continue your journey:

See also