Message Inbox
Persistent message center — store messages users can revisit. Send to user/users/broadcast, message fields, SDK integration on Flutter/RN/Android/Web, building an inbox UI.
A persistent message center for your users. Unlike push notifications, inbox messages are stored and can be retrieved later — users never miss important updates.
Why Use Message Inbox?#
Persistent Storage
Messages are stored and accessible anytime, unlike ephemeral push notifications
Read Tracking
Know when users read your messages with built-in analytics
Category Filtering
Organize messages by category (promotions, updates, alerts)
User Control
Users can archive, delete, or mark messages as read
Use Cases#
Announcements
Product updates, new features, company news that users should be able to reference later.
Promotions
Special offers, discounts, and deals with expiration dates that users can redeem at their convenience.
Transactional
Order updates, account alerts, and important notifications that need to be preserved.
Using the Dashboard#
In Abrova Console, open AbrovaPush → Inbox. It provides four ways to send messages:
Send a message to a specific user by their ID
Send a message to a specific device by its device ID
Send to multiple users by providing a comma-separated list of user IDs
Send to all users of your app at once
Send to User#
Node.js SDK
import { AbrovaPush } from '@abrova/push-node';
const abrovaPush = new AbrovaPush({
apiKey: 'ab_live_your_api_key',
serverSecret: 'ab_srv_your_server_secret',
});
await abrovaPush.inbox.sendToUser({
userId: 'user_123',
content: {
title: 'New Promotion!',
body: 'Get 20% off your next purchase',
imageUrl: 'https://example.com/promo.jpg',
deepLink: 'myapp://promotions/123',
},
category: 'promotions',
expiresAt: '2024-02-01T00:00:00Z',
});REST API
curl -X POST "https://push-api.abrova.ir/inbox/api/send-to-user" \
-H "x-api-key: ab_live_your_api_key" \
-H "x-server-secret: ab_srv_your_server_secret" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"content": {
"title": "New Promotion!",
"body": "Get 20% off your next purchase",
"imageUrl": "https://example.com/promo.jpg",
"deepLink": "myapp://promotions/123"
},
"category": "promotions",
"expiresAt": "2024-02-01T00:00:00Z"
}'Send to Multiple Users#
Node.js SDK
await abrovaPush.inbox.sendToUsers({
userIds: ['user_123', 'user_456', 'user_789'],
content: {
title: 'Flash Sale!',
body: 'Limited time offer for VIP members',
},
category: 'promotions',
});REST API
curl -X POST "https://push-api.abrova.ir/inbox/api/send-to-users" \
-H "x-api-key: ab_live_your_api_key" \
-H "x-server-secret: ab_srv_your_server_secret" \
-H "Content-Type: application/json" \
-d '{
"userIds": ["user_123", "user_456", "user_789"],
"content": {
"title": "Flash Sale!",
"body": "Limited time offer for VIP members"
},
"category": "promotions"
}'Broadcast to All Users#
Node.js SDK
await abrovaPush.inbox.broadcast({
content: {
title: 'App Update Available',
body: 'We have released new features! Check them out.',
},
category: 'announcements',
});REST API
curl -X POST "https://push-api.abrova.ir/inbox/api/broadcast" \
-H "x-api-key: ab_live_your_api_key" \
-H "x-server-secret: ab_srv_your_server_secret" \
-H "Content-Type: application/json" \
-d '{
"content": {
"title": "App Update Available",
"body": "We have released new features! Check them out."
},
"category": "announcements"
}'Message Fields#
| Option | Type | Required | Description |
|---|---|---|---|
| content.title | string | Yes | Message title |
| content.body | string | Yes | Message body |
| content.imageUrl | string | No | Image URL to display with message |
| content.iconUrl | string | No | Small icon/avatar for the message |
| content.deepLink | string | No | Deep link to open when message is tapped |
| content.data | object | No | Custom key/value data for your app |
| localizations | array | No | Per-locale content: [{ locale, content }] |
| category | string | No | Category for filtering (e.g., "promotions", "orders") |
| expiresAt | ISO 8601 | No | Expiration date (message hidden after this date) |
Abrova Console has a project setting, Require signed user tokens for the inbox (Project Settings → Abrova Push). With it on, the inbox read/update routes the SDKs call only serve a user's messages when the request carries an x-user-token minted by your server; otherwise they return 401 with code: "token_required". The Push client SDKs below do not send this header yet, so leave the setting off unless your app calls the inbox API itself with the token. Device-only inboxes and push delivery are not affected.
Flutter#
// Fetch inbox messages
final response = await AbrovaPush.getInboxMessages(
status: InboxMessageStatus.unread, // omit for all; unread, read, archived
category: 'promotions', // optional filter
limit: 20,
offset: 0,
);
print('Total: ${response.total}, Unread: ${response.unreadCount}');
// Display messages in your UI
for (final message in response.messages) {
print('Title: ${message.content.title}');
print('Body: ${message.content.body}');
print('Status: ${message.status}');
print('Read at: ${message.readAt}');
}
// Mark message as read
await AbrovaPush.markInboxMessageAsRead(message.id);
// Mark multiple messages with a status
await AbrovaPush.markMultipleInboxMessages(
['msg_1', 'msg_2', 'msg_3'],
InboxMessageStatus.read,
);
// Mark all messages as read
await AbrovaPush.markAllInboxMessagesAsRead();
// Archive a message
await AbrovaPush.archiveInboxMessage(message.id);
// Delete a message
await AbrovaPush.deleteInboxMessage(message.id);
// Get unread count for badge (cached; fetchInboxUnreadCount() asks the server)
final unreadCount = await AbrovaPush.getInboxUnreadCount();
print('Unread messages: $unreadCount');
// Listen to new inbox messages
AbrovaPush.onInboxMessage((message) {
print('New inbox message: ${message.content.title}');
// Update your UI or show a badge
});React Native#
import AbrovaPush, { InboxMessageStatus } from 'abrova-push-react-native';
// Fetch inbox messages
const { messages, total, unreadCount } = await AbrovaPush.getInboxMessages({
status: InboxMessageStatus.UNREAD, // omit for all; UNREAD, READ, ARCHIVED
category: 'promotions', // optional filter
limit: 20,
offset: 0,
});
// Display in FlatList or custom UI
messages.forEach(message => {
console.log('Title:', message.content.title);
console.log('Body:', message.content.body);
console.log('Status:', message.status);
});
// Mark message as read
await AbrovaPush.markInboxMessageAsRead(message.id);
// Mark multiple messages with a status
await AbrovaPush.markMultipleInboxMessages(['msg_1', 'msg_2'], InboxMessageStatus.READ);
// Mark all messages as read
await AbrovaPush.markAllInboxMessagesAsRead();
// Archive a message
await AbrovaPush.archiveInboxMessage(message.id);
// Delete a message
await AbrovaPush.deleteInboxMessage(message.id);
// Get unread count for badge (cached; fetchInboxUnreadCount() asks the server)
const badgeCount = await AbrovaPush.getInboxUnreadCount();
console.log('Unread:', badgeCount);
// Cached messages, no network call
const cached = await AbrovaPush.getCachedInboxMessages();
// Note: the React Native wrapper has no live inbox listener yet.
// Refresh with getInboxMessages() / fetchInboxUnreadCount() (e.g. on screen focus).Android#
// Fetch inbox messages
val filter = InboxFilter(
status = InboxMessageStatus.UNREAD, // null for all
category = "promotions", // optional
limit = 20,
offset = 0
)
AbrovaPush.getInboxMessages(
filter = filter,
onSuccess = { response ->
Log.d("Inbox", "Total: ${response.total}, Unread: ${response.unreadCount}")
response.messages.forEach { message ->
Log.d("Inbox", "Title: ${message.content.title}")
Log.d("Inbox", "Body: ${message.content.body}")
Log.d("Inbox", "Status: ${message.status}")
}
},
onError = { error ->
Log.e("Inbox", "Failed to fetch: $error")
}
)
// Mark message as read
AbrovaPush.markInboxMessageAsRead(
messageId = message.id,
onSuccess = { Log.d("Inbox", "Marked as read") },
onError = { error -> Log.e("Inbox", "Error: $error") }
)
// Mark all messages as read
AbrovaPush.markAllInboxMessagesAsRead(
onSuccess = { Log.d("Inbox", "All marked as read") },
onError = { error -> Log.e("Inbox", "Error: $error") }
)
// Archive a message
AbrovaPush.archiveInboxMessage(
messageId = message.id,
onSuccess = { Log.d("Inbox", "Archived") },
onError = { error -> Log.e("Inbox", "Error: $error") }
)
// Delete a message
AbrovaPush.deleteInboxMessage(
messageId = message.id,
onSuccess = { Log.d("Inbox", "Deleted") },
onError = { error -> Log.e("Inbox", "Error: $error") }
)
// Get unread count (from cache, synchronous)
val unreadCount = AbrovaPush.getInboxUnreadCount()
Log.d("Inbox", "Unread: $unreadCount")
// Fetch unread count from server
AbrovaPush.fetchInboxUnreadCount(
onSuccess = { count -> updateBadge(count) },
onError = { error -> Log.e("Inbox", "Error: $error") }
)
// Get cached messages (no network call)
val cachedMessages = AbrovaPush.getCachedInboxMessages()
// Set callback for new inbox messages
AbrovaPush.setInboxCallback(object : InboxCallback {
override fun onMessageReceived(message: InboxMessage) {
Log.d("Inbox", "New message: ${message.content.title}")
// Update your UI or show badge
}
override fun onMessageStatusChanged(messageId: String, status: InboxMessageStatus) {
Log.d("Inbox", "Message $messageId status: $status")
}
})iOS#
import AbrovaPush
// Fetch inbox messages
let filter = InboxFilter(
status: .unread, // nil for all
category: "promotions", // optional
limit: 20,
offset: 0
)
AbrovaPush.shared.getInboxMessages(
filter: filter,
onSuccess: { response in
print("Total: \(response.total), Unread: \(response.unreadCount)")
for message in response.messages {
print("Title: \(message.content.title)")
print("Body: \(message.content.body)")
print("Status: \(message.status.rawValue)")
}
},
onError: { error in
print("Failed to fetch: \(error)")
}
)
// Mark message as read
AbrovaPush.shared.markInboxMessageAsRead(
messageId: message.id,
onSuccess: { print("Marked as read") },
onError: { error in print("Error: \(error)") }
)
// Mark multiple messages with a status
AbrovaPush.shared.markMultipleInboxMessages(
messageIds: ["msg_1", "msg_2", "msg_3"],
status: .read,
onSuccess: { print("Updated") },
onError: { error in print("Error: \(error)") }
)
// Mark all messages as read
AbrovaPush.shared.markAllInboxMessagesAsRead(
onSuccess: { print("All marked as read") },
onError: { error in print("Error: \(error)") }
)
// Archive a message
AbrovaPush.shared.archiveInboxMessage(
messageId: message.id,
onSuccess: { print("Archived") },
onError: { error in print("Error: \(error)") }
)
// Delete a message
AbrovaPush.shared.deleteInboxMessage(
messageId: message.id,
onSuccess: { print("Deleted") },
onError: { error in print("Error: \(error)") }
)
// Get unread count (from cache, synchronous)
let unreadCount = AbrovaPush.shared.getInboxUnreadCount()
print("Unread: \(unreadCount)")
// Fetch unread count from server
AbrovaPush.shared.fetchInboxUnreadCount(
onSuccess: { count in updateBadge(count) },
onError: { error in print("Error: \(error)") }
)
// Get cached messages (no network call)
let cachedMessages = AbrovaPush.shared.getCachedInboxMessages()
// Set callback for new inbox messages
class MyInboxHandler: InboxCallback {
func inboxMessageReceived(_ message: InboxMessage) {
print("New message: \(message.content.title)")
// Update your UI or show badge
}
func inboxMessageStatusChanged(messageId: String, status: InboxMessageStatus) {
print("Message \(messageId) status: \(status.rawValue)")
}
}
// The callback is held weakly, so keep a strong reference (e.g. a property).
let handler = MyInboxHandler()
AbrovaPush.shared.setInboxCallback(handler)Web#
Requires @abrova/push-web 0.1.6 or newer, and a registered device.
import AbrovaPush from '@abrova/push-web';
const abrovaPush = new AbrovaPush({ apiKey: 'ab_live_your_api_key' });
await abrovaPush.register({ userId: 'user_123' });
// Live updates — an inbox message never shows a notification
abrovaPush.inbox.onMessage((message) => {
console.log('New:', message.content.title);
});
abrovaPush.inbox.onUnreadCountChange((count) => {
document.querySelector('.badge').textContent = String(count);
});
// Read
const { messages, total, unreadCount } = await abrovaPush.inbox.getMessages({
status: 'unread',
category: 'promotions',
limit: 20,
});
const cached = abrovaPush.inbox.getCachedMessages(); // instant, no network
// Write
await abrovaPush.inbox.markAsRead(messages[0].id);
await abrovaPush.inbox.archiveMessage(messages[0].id);
await abrovaPush.inbox.deleteMessage(messages[0].id);
await abrovaPush.inbox.markMultiple(['msg_1', 'msg_2'], 'read');
await abrovaPush.inbox.markAllAsRead();
// Badge count without a fetch
const count = abrovaPush.inbox.getUnreadCount();Messages are cached per device and restored on the next page load, so an inbox
screen renders instantly and refreshes in the background. The cache is dropped
when the signed-in user changes (setUserId / clearUserId).
Building an Inbox UI (Flutter)#
class InboxScreen extends StatefulWidget {
@override
_InboxScreenState createState() => _InboxScreenState();
}
class _InboxScreenState extends State<InboxScreen> {
List<InboxMessage> messages = [];
bool loading = true;
@override
void initState() {
super.initState();
loadMessages();
}
Future<void> loadMessages() async {
final result = await AbrovaPush.getInboxMessages();
setState(() {
messages = result.messages;
loading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Inbox')),
body: loading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return ListTile(
leading: message.content.imageUrl != null
? Image.network(message.content.imageUrl!, width: 50)
: Icon(Icons.mail),
title: Text(
message.content.title,
style: TextStyle(
fontWeight: message.status == InboxMessageStatus.unread
? FontWeight.bold
: FontWeight.normal,
),
),
subtitle: Text(message.content.body, maxLines: 2),
trailing: message.status == InboxMessageStatus.unread
? Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
)
: null,
onTap: () async {
await AbrovaPush.markInboxMessageAsRead(message.id);
if (message.content.deepLink != null) {
// Navigate to deep link
}
loadMessages();
},
);
},
),
);
}
}Best Practices#
- 1.Show unread badge — Display an unread count badge on your inbox icon to encourage users to check messages.
- 2.Use categories — Organize messages into categories so users can filter and find relevant content.
- 3.Set expiration dates — Use expiresAt for time-sensitive offers to automatically hide outdated content.
- 4.Include deep links — Always add a deep link so users can take action directly from the message.
- 5.Mark as read — Automatically mark messages as read when opened to keep the unread count accurate.
- 6.Paginate results — Use limit and offset parameters when fetching messages to handle large inboxes efficiently.