Unread counts and read state
Query users' unread counts, or mark conversations as read on their behalf, from your server.
| You want to | API |
|---|---|
| Remind a user by SMS or email when they haven't signed in for a while but have unread messages | Get a user's total unread count |
| A user already saw this notice on your website or in an email | Mark a conversation as read |
| Clear all of a user's unread messages at once | Mark everything as read |
For example, how many unread messages bob has:
curl "$IM_API/$ORG/$APP/users/bob/unread-count" \
-H "Authorization: Bearer $APP_TOKEN"
# {"total":3,"conversation_count":1,"marked_unread_count":1,"truncated":false}The server keeps a read position for every user in every conversation (read_seq in the user conversation object) and computes unread counts from it. Messages a client hasn't fetched count too, so the server's unread counts are always accurate.
Marking as read for a user is a real read, just like the user marking it in a client: all their devices update, the other side of a one-to-one chat sees the new read position, and group messages that asked for receipts count it as read.
How unread counts work
Unread count = the messages after the read position that the user can see and that count as unread. The read position only moves forward: reading a later message means the earlier ones are read too.
Messages that don't count as unread:
- Group notices.
- Messages sent with
exclude_from_unreadset totrue. - Messages the user sent from a client.
- Messages deleted after passing retention.
How operations affect unread state:
| Operation | Read position | Unread count |
|---|---|---|
| Your server sends as the user, who had nothing unread | Moves to this message | Stays 0 |
| Your server sends as the user, who had unread messages | Unchanged | This message counts as unread for them too, see The sender's unread count |
| The user deletes messages they haven't read | Unchanged | They still count until the read position passes them. When deleting for a user, also mark as read |
| Deleting the conversation, clearing chat history | Moves to the last message at that time | Reset to 0 |
| The user marks it unread in a client | Unchanged | Unchanged; only marked_unread becomes true, and marking as read clears it |
For a group message that mentioned the user or everyone, mention_seq holds its sequence until the user reads it, then becomes null.
What marking as read updates
Marking as read, and deleting a conversation or clearing chat history (which also move the read position), have these effects:
| Who | Mark as read, delete conversation, clear chat history | Mark everything as read |
|---|---|---|
| The user's online devices | Get the conversation's new state in real time; unread counts and mention reminders update | Get a notice to resync, then sync the conversation list |
| The user's offline devices | Update on next sign-in or sync | Same |
| The other side of a one-to-one chat | peer_read_seq in their conversation moves to the new read position, and their online devices get a read notice in real time | peer_read_seq moves too, but without a real-time notice; they see it on their next sync |
| Group messages that asked for receipts | Count as read (users who left the group don't count); in groups of up to 500, the sender gets a read notice in real time | Same |
- Settings: one-to-one read state is controlled by
single_read_ack_enabledin the runtime policy (on by default); when off,peer_read_seqisn't updated or returned and the other side isn't notified. Group read receipts are controlled bygroup_read_ack_enabled(off by default) and only apply to messages that asked for receipts. - Repeated marking: if the read position doesn't move and the other side already has the latest, nobody is notified and receipts don't change.
- Sending as a user isn't reading: it doesn't move the read position the other side sees and doesn't count toward group receipts. To show they've read it, also call Mark a conversation as read.
Get a user's total unread count
Get the sum of unread counts across a user's conversations and how many conversations have unread messages, for app icon badges or unread reminders.
/{org_name}/{app_name}/users/{username}/unread-countWhat's counted:
- Every group the user is currently in.
- The 1000 most recent one-to-one chats and left groups (by last message time). Beyond that only those 1000 are counted and
truncatedistrue. - Conversations the user deleted (
hiddentrue) aren't counted.
Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username, case-insensitive |
Request example
curl "$IM_API/$ORG/$APP/users/bob/unread-count" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK:
| Field | Type | Description |
|---|---|---|
total | Number | Total unread messages across conversations |
conversation_count | Number | Conversations with unread messages |
marked_unread_count | Number | Conversations without unread messages that the user marked unread |
truncated | Boolean | More than 1000 one-to-one chats and left groups; only the latest 1000 were counted |
{
"total": 3,
"conversation_count": 1,
"marked_unread_count": 1,
"truncated": false
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted |
Mark a conversation as read
Mark a conversation as read for a user: move their read position to the given message and clear "marked unread". For the effects, see What marking as read updates.
/{org_name}/{app_name}/users/{username}/conversations/{conversation_id}/readGroups the user has left can be marked as read too; it only changes the user's own state.
Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username, case-insensitive |
conversation_id | String | The conversation ID; for groups, the group ID |
Request body
The body is optional; without it, everything up to the newest message is marked read.
| Field | Type | Required | Description |
|---|---|---|---|
seq | Number | No | The sequence read up to. Omitted or null means the newest message the user can see; above the newest counts as the newest; at or below the current read position leaves it unchanged |
Request example
curl -X POST "$IM_API/$ORG/$APP/users/bob/conversations/99582660229201920/read" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"seq": 5
}'Response
On success you get 200 OK:
| Field | Type | Description |
|---|---|---|
conversation | Object | The user conversation object after the operation |
changed | Boolean | Whether anything changed. false when the read position didn't move, "marked unread" wasn't cleared and the other side's view needed no update, so retries are safe |
{
"conversation": {
"conversation_id": "99582660229201920",
"conversation_type": "single",
"peer": "alice",
"group_id": null,
"membership": null,
"start_seq": 1,
"max_seq": 10,
"read_seq": 5,
"unread_count": 5,
"marked_unread": false,
"peer_read_seq": 0,
"mention_seq": null,
"cleared_seq": 0,
"hidden_seq": 0,
"hidden": false,
"pinned_at": null,
"joined_at": null,
"left_at": null,
"message_version": 0,
"last_message_at": "2026-10-02T19:07:06.211Z",
"version": 12
},
"changed": true
}Afterwards, peer_read_seq in alice's conversation becomes 5, and her online devices are told in real time that bob has read up to message 5.
Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | The body isn't a JSON object, seq isn't a number, or there are other fields |
| 404 | not_found | The user doesn't exist or was deleted; the conversation doesn't exist; the user isn't part of the one-to-one chat; the group doesn't exist, or is private and the user never joined |
| 403 | not_group_member | A public group the user isn't in and has no record of leaving |
| 403 | not_group_member | details.reason leave_pending: the user just left and it hasn't finished processing; retry shortly |
Mark everything as read
Mark all of a user's unread conversations as read, over the same range as the total unread count: conversations with unread messages or marked unread among their current groups and the 1000 most recent one-to-one chats and left groups.
/{org_name}/{app_name}/users/{username}/conversations/read-allThe user's online devices get a notice to resync. The other sides of one-to-one chats don't get individual read notices; they see the new read positions on their next sync, see What marking as read updates.
Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username, case-insensitive |
Request example
curl -X POST "$IM_API/$ORG/$APP/users/bob/conversations/read-all" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK:
| Field | Type | Description |
|---|---|---|
count | Number | How many conversations were marked read; 0 when none had unread messages |
truncated | Boolean | More than 1000 one-to-one chats and left groups, so older ones weren't processed. Handle them one by one with Mark a conversation as read |
{
"count": 2,
"truncated": false
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted |
