Skip to main content

In-App Inbox

Get Inbox Session

Get a Novu session token and WebSocket URL for real-time inbox updates.

POST /api/v1/notifications/get-inbox-session
{}

Response:

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"socketUrl": "wss://novu-ws.yocaso.dev",
"expiresIn": "1296000"
}
FieldTypeDescription
tokenstringJWT for WebSocket auth
socketUrlstringNovu WebSocket URL
expiresInstringToken validity in seconds (~15 days). String type (int64 protojson convention).

Real-Time Inbox Updates

The socketUrl + token from get-inbox-session open a Socket.IO connection (Novu) that pushes inbox changes live — no polling needed for the common case. Authenticate with the session token, both as an Authorization: Bearer <token> header and as a token connect param.

The server emits these events:

EventFires when
notification_receivedA new message arrives — refresh the feed or prepend the payload
unseen_count_changedThe unseen badge count changes
unread_count_changedThe unread count changes

Novu payloads may identify a message as either _id or messageId — accept both.

Resilience. If the socket fails to connect or drops, reconnect with exponential backoff (a handful of attempts), and while disconnected fall back to polling get-inbox-unseen-count every ~30 seconds so the badge stays roughly current. The session token expires (~15 days) — call get-inbox-session again to refresh it.

Get Inbox Feed

POST /api/v1/notifications/get-inbox-feed
{
"page": 1,
"pageSize": 20
}

Pagination is 1-based. The optional filter object supports unseenOnly, unreadOnly, feedIds, categories and tags (plain strings), plus archived — which is how you render an archive view:

{
"page": 1,
"pageSize": 20,
"filter": {"archived": true}
}

Archived messages are excluded from the default feed, so omitting archived gives you the live inbox.

Response:

{
"messages": [
{
"messageId": "69c0df5391079c0a4596f7d3",
"notificationId": "69c0df5291079c0a4596f79b",
"title": "Weekly Check-in",
"body": "Time to review your progress.",
"data": {
"title": "Weekly Check-in",
"body": "Time to review your progress.",
"deep_link": "socayo://screen?name=weekly"
},
"deepLink": "socayo://screen?name=weekly",
"status": "MESSAGE_STATUS_UNSEEN",
"createdAt": "2026-03-23T06:36:03.578Z"
}
],
"totalCount": 6,
"page": 1,
"pageSize": 20
}

hasMore only appears when true. Fields like category, deepLink, actions are omitted when empty.

Get Unseen Count

POST /api/v1/notifications/get-inbox-unseen-count

Response:

{
"unseenCount": 3,
"unreadCount": 5
}

Counts are capped at 10. hasMoreUnseen/hasMoreUnread appear when true — display as "9+".

Mark Messages

Mark a single message:

POST /api/v1/notifications/mark-inbox-message
{
"messageId": "msg_123",
"markAs": "MESSAGE_STATUS_READ"
}

Response:

{
"messages": [
{
"messageId": "msg_123",
"channel": "in_app",
"seen": true,
"content": "Time to review your progress.",
"status": "sent",
"createdAt": "2026-03-23T06:36:03.578Z",
"lastSeenDate": "2026-03-23T10:24:35.518Z"
}
]
}

Mark all messages:

POST /api/v1/notifications/mark-all-inbox-messages
{
"markAs": "MESSAGE_STATUS_SEEN"
}

Response: {"updatedCount": 5}

Status options: MESSAGE_STATUS_SEEN, MESSAGE_STATUS_READ, MESSAGE_STATUS_UNSEEN, MESSAGE_STATUS_UNREAD.

Delete Inbox Message

POST /api/v1/notifications/delete-inbox-message
{
"messageId": "msg_123"
}

Response:

{
"status": {
"acknowledged": true,
"status": "deleted"
}
}

Archive Messages

Archiving hides a message from the default feed but keeps it — unlike delete-inbox-message, it is reversible.

messageId vs notificationId

The archive and action endpoints below are addressed by notificationId, not messageId. Both appear on every feed entry and they are different values — sending a messageId here fails to find the message. Only mark-inbox-message and delete-inbox-message take messageId.

POST /api/v1/notifications/archive-inbox-message
{
"notificationId": "69c0df5291079c0a4596f79b"
}

Response:

{
"status": {
"acknowledged": true,
"status": "archived"
}
}

Restore it with the mirror call:

POST /api/v1/notifications/unarchive-inbox-message
{
"notificationId": "69c0df5291079c0a4596f79b"
}

Response: the same envelope, with status: "unarchived".

A message that isn't there is a success

Every endpoint in this section treats "not found" as idempotent success, not a 404: you get acknowledged: true with status: "not_found". So a client can archive on every tap without first checking, and a retry after a dropped response is safe. Branch on status === "not_found" if you need to tell the two apart.

Archive in Bulk

Clear the whole inbox, or just the messages the user has already read — the second is what a "clear read" button should call.

POST /api/v1/notifications/archive-all-inbox-messages
{}

Response: {"archivedCount": 12}

POST /api/v1/notifications/archive-all-read-inbox-messages
{}

Response: {"archivedCount": 7}

Both accept an optional tags array to scope the sweep to particular workflow tags, OR-ed together:

{
"tags": ["coaching", "reminders"]
}

Message Actions

A message can carry up to two call-to-action buttons. Each entry from POST /api/v1/notifications/get-inbox-feed exposes them under actions, with isPrimary marking which is which:

{
"actions": [
{
"actionId": "log-workout",
"label": "Log it",
"url": "travila://workout/new",
"isPrimary": true,
"completed": false
}
]
}

When the user taps one, record it so the button renders as done — and so it stays done on the user's other devices.

POST /api/v1/notifications/complete-inbox-action
{
"notificationId": "69c0df5291079c0a4596f79b",
"actionType": "ACTION_TYPE_PRIMARY"
}

Response:

{
"status": {
"acknowledged": true,
"status": "completed"
}
}

actionType is ACTION_TYPE_PRIMARY or ACTION_TYPE_SECONDARY. To undo — the user cancelled the flow the button opened, or the write it represented failed — revert it:

POST /api/v1/notifications/revert-inbox-action
{
"notificationId": "69c0df5291079c0a4596f79b",
"actionType": "ACTION_TYPE_PRIMARY"
}

Response: the same envelope, with status: "reverted" and the action back in its pending state.