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: Android SDK 1.0.0.

This guide covers accessing the inbox, fetching and querying messages, updating read/unread status, handling CTA buttons, and clearing messages.

Note: To create App Inbox campaigns, see User Guide: App Inbox for campaign setup from ZMP.

Note: fetchMessages uses a single onRead callback. Other ZTAppInboxManager methods (markMessageAsRead, getUnreadMessages, etc.) use onSuccess and onError lambdas and are invoked on the main thread. Java consumers use SAM conversions (functional interfaces).

On this page

Access the inbox

The inbox is exposed through ZetaClient.inbox, which returns an object implementing ZTAppInboxManager.

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

val inbox = ZetaClient.inbox
ZTAppInboxManager inbox = ZetaClient.INSTANCE.getInbox();

Fetch messages

fetchMessages() syncs the server state into the cache and returns the full list of non-deleted, non-expired messages.

Note: The local store retains up to 200 active (non-deleted, non-expired) messages. The remote backup contains the most recent 50 messages. If a sync would increase the local count beyond 200, the oldest active messages are evicted from the local store.

ZetaClient.inbox.fetchMessages { messages ->
    updateUI(messages)
}
ZetaClient.INSTANCE.getInbox().fetchMessages(messages -> {
    updateUI(messages);
});

Query cached messages

After a fetch, query cached messages without a network round-trip.

ZetaClient.inbox.getUnreadMessages(
    onSuccess = { messages -> /* handle unread messages */ },
    onError = { error -> /* handle error */ }
)

ZetaClient.inbox.getReadMessages(
    onSuccess = { messages -> /* handle read messages */ },
    onError = { error -> /* handle error */ }
)

ZetaClient.inbox.getMessageCount(
    onSuccess = { count -> /* total count */ },
    onError = { error -> /* handle error */ }
)

ZetaClient.inbox.getUnreadMessageCount(
    onSuccess = { count -> updateBadge(count) },
    onError = { error -> /* handle error */ }
)

ZetaClient.inbox.getReadMessageCount(
    onSuccess = { count -> /* read count */ },
    onError = { error -> /* handle error */ }
)

ZetaClient.inbox.getMessage(
    messageId = "MSG-001",
    onSuccess = { message -> /* handle message */ },
    onError = { error -> /* handle error */ }
)
ZetaClient.INSTANCE.getInbox().getUnreadMessages(
    messages -> { /* handle unread messages */ },
    error -> { /* handle error */ }
);

ZetaClient.INSTANCE.getInbox().getReadMessages(
    messages -> { /* handle read messages */ },
    error -> { /* handle error */ }
);

ZetaClient.INSTANCE.getInbox().getMessageCount(
    count -> { /* total count */ },
    error -> { /* handle error */ }
);

ZetaClient.INSTANCE.getInbox().getUnreadMessageCount(
    count -> updateBadge(count),
    error -> { /* handle error */ }
);

ZetaClient.INSTANCE.getInbox().getReadMessageCount(
    count -> { /* read count */ },
    error -> { /* handle error */ }
);

ZetaClient.INSTANCE.getInbox().getMessage(
    "MSG-001",
    message -> { /* handle message */ },
    error -> { /* handle error */ }
);

The ZTAppInboxMessage model

ZTAppInboxMessage represents a single inbox entry.

PropertyTypeDescription
messageIdStringUnique identifier.
titleStringMessage headline.
bodyStringMessage body text.
mediaUrlString?Optional image or media URL.
statusZTAppInboxMessageStatusCurrent READ, UNREAD, or DELETED state.
expirationTimestampLong?Unix timestamp in milliseconds after which the message should not be shown.
actionListList<ZTCtaAction>?List of CTA buttons attached to the message.
templateIdStringTemplate identifier used to decide the UI style of the message (for example, to switch between a banner row and a media row in a RecyclerView adapter).
additionalDataMap<String, String>Arbitrary key-value pairs from the campaign in ZMP — for example, {"category": "promo", "variant": "B"}.

Each message has one of three statuses, persisted locally: UNREAD, READ, and DELETED. New messages arrive as UNREAD. Move a message to READ after the user views it, or to DELETED when the user dismisses it. Status changes are maintained locally.

If the cache is cleared — for example, on opt-out or clearAll() — all statuses reset. The next fetchMessages() repopulates from the server, and every message returns as UNREAD.

enum class ZTAppInboxMessageStatus { READ, UNREAD, DELETED }
ValueMeaning
UNREADThe user has not opened or interacted with the message yet. This is the default state for a freshly received inbox message.
READThe user has seen 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. The SDK remembers the deletion so the message is not re-fetched on the next sync.

Update message status

Mark as read

ZetaClient.inbox.markMessageAsRead(
    messageId = "MSG-001",
    onSuccess = { message -> /* updated message */ },
    onError = { error -> /* handle error */ }
)

val ids = messages.map { it.messageId }
ZetaClient.inbox.markAllMessagesAsRead(
    ids = ids,
    onSuccess = { updatedMessages -> /* list of updated messages */ },
    onError = { error -> /* handle error */ }
)
ZetaClient.INSTANCE.getInbox().markMessageAsRead(
    "MSG-001",
    message -> { /* updated message */ },
    error -> { /* handle error */ }
);

List<String> ids = new ArrayList<>();
for (ZTAppInboxMessage msg : messages) {
    ids.add(msg.getMessageId());
}
ZetaClient.INSTANCE.getInbox().markAllMessagesAsRead(
    ids,
    updatedMessages -> { /* list of updated messages */ },
    error -> { /* handle error */ }
);

Mark as deleted

ZetaClient.inbox.markMessageAsDeleted(
    messageId = "MSG-001",
    onSuccess = { message -> /* updated message */ },
    onError = { error -> /* handle error */ }
)

ZetaClient.inbox.markAllMessagesAsDeleted(
    ids = ids,
    onSuccess = { updatedMessages -> /* list of updated messages */ },
    onError = { error -> /* handle error */ }
)
ZetaClient.INSTANCE.getInbox().markMessageAsDeleted(
    "MSG-001",
    message -> { /* updated message */ },
    error -> { /* handle error */ }
);

ZetaClient.INSTANCE.getInbox().markAllMessagesAsDeleted(
    ids,
    updatedMessages -> { /* list of updated messages */ },
    error -> { /* handle error */ }
);

Deleted messages are excluded from future query results. markAllMessagesAsRead() and markAllMessagesAsDeleted() return the list of messages that were successfully updated.

Clear all messages

ZetaClient.inbox.clearAll(
    onSuccess = { /* all messages cleared */ },
    onError = { error -> /* handle error */ }
)
ZetaClient.INSTANCE.getInbox().clearAll(
    success -> { /* all messages cleared */ },
    error -> { /* handle error */ }
);

Use clearAll() on logout or account switch. All messages and their statuses are removed from the cache. The next fetchMessages() call repopulates from the server.

Handle CTA buttons

Messages may contain one or more call-to-action buttons, each represented by ZTCtaAction. When the user taps a button, call onMessageClicked() with the action's value (or link) to record the click and trigger any server-side automation.

data class ZTCtaAction(
    val type: String,
    val text: String,
    val value: String,
    val link: String,
    val actionType: String = ""
)
FieldDescription
textButton label shown to the user (for example, "Open", "Learn more").
typeAction type identifier from the campaign template.
valuePrimary action value, used as the actionValue parameter in onMessageClicked().
linkURL associated with the action. May be a deep link or web URL.
actionTypeAdditional action-type metadata. Use it to disambiguate behavior when several CTAs share the same type — for example, when type is "click", actionType can decide whether to open the link in the in-app browser or the system browser.
fun handleCTA(action: ZTCtaAction, message: ZTAppInboxMessage) {
    val actionValue = if (action.value.isNotEmpty()) action.value else action.link

    ZetaClient.inbox.onMessageClicked(
        messageId = message.messageId,
        actionValue = actionValue,
        onSuccess = { updatedMessage -> /* handle success */ },
        onError = { error -> /* handle error */ }
    )

    if (action.link.isNotEmpty()) {
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(action.link)))
    }
}
public void handleCTA(ZTCtaAction action, ZTAppInboxMessage message) {
    String actionValue = !action.getValue().isEmpty() ? action.getValue() : action.getLink();

    ZetaClient.INSTANCE.getInbox().onMessageClicked(
        message.getMessageId(),
        actionValue,
        updatedMessage -> { /* handle success */ },
        error -> { /* handle error */ }
    );

    if (!action.getLink().isEmpty()) {
        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(action.getLink())));
    }
}

Putting it all together

The following example shows a minimal but complete Jetpack Compose inbox screen: fetch messages on launch, display an unread badge on the entry point, mark as read when the user taps a message, and handle a CTA tap.

import androidx.compose.runtime.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun InboxScreen() {
    var messages by remember { mutableStateOf<List<ZTAppInboxMessage>>(emptyList()) }
    var unreadCount by remember { mutableIntStateOf(0) }

    LaunchedEffect(Unit) {
        ZetaClient.inbox.fetchMessages { fetched ->
            messages = fetched
        }
        ZetaClient.inbox.getUnreadMessageCount(
            onSuccess = { count -> unreadCount = count },
            onError = { /* handle */ }
        )
    }

    LazyColumn {
        items(messages, key = { it.messageId }) { message ->
            InboxRow(
                message = message,
                onTap = {
                    ZetaClient.inbox.markMessageAsRead(
                        messageId = message.messageId,
                        onSuccess = {
                            messages = messages.map { m ->
                                if (m.messageId == message.messageId) it else m
                            }
                            unreadCount = maxOf(0, unreadCount - 1)
                        },
                        onError = { /* handle */ }
                    )
                },
                onCTA = { action ->
                    val actionValue = if (action.value.isNotEmpty()) action.value else action.link
                    ZetaClient.inbox.onMessageClicked(
                        messageId = message.messageId,
                        actionValue = actionValue,
                        onSuccess = { /* recorded */ },
                        onError = { /* handle */ }
                    )
                    if (action.link.isNotEmpty()) {
                        // navigate to action.link — deep link or web URL
                    }
                }
            )
        }
    }
}

@Composable
fun InboxRow(
    message: ZTAppInboxMessage,
    onTap: () -> Unit,
    onCTA: (ZTCtaAction) -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onTap() }
            .padding(16.dp)
    ) {
        Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
            if (message.status == ZTAppInboxMessageStatus.UNREAD) {
                Badge(modifier = Modifier.padding(end = 8.dp)) { }
            }
            Text(message.title, style = MaterialTheme.typography.titleMedium)
        }
        Spacer(Modifier.height(4.dp))
        Text(message.body, style = MaterialTheme.typography.bodyMedium)
        message.actionList?.forEach { action ->
            OutlinedButton(
                onClick = { onCTA(action) },
                modifier = Modifier.padding(top = 8.dp)
            ) { Text(action.text) }
        }
    }
}

Key points:

  • Call fetchMessages in a LaunchedEffect so messages load once when the screen enters composition.
  • Refresh unreadCount separately via getUnreadMessageCount to drive any entry-point badge (e.g., a bottom-nav tab).
  • Mark as read immediately on tap for a responsive UI — status changes are persisted locally and do not require a server round-trip.
  • Pass action.value (not action.link) to onMessageClicked(). Use action.link only to navigate if it is non-empty.
  • Check message.additionalData for any campaign-defined routing keys before applying display customization (for example, switching between a banner row and a media row in your adapter).

Next

See also