Conversations and history
Query conversations and chat history from your server, typically for archiving, letting support staff read chats, or handling complaints.
| You want to | API |
|---|---|
| Read the chat between alice and bob | First get the one-to-one conversation between two users for its conversation_id, then fetch the conversation's history |
| Read a group's chat | The group ID is the conversation ID, so fetch the conversation's history directly |
| Export one day's chat | Fetch by time range |
| See a user's conversation list and unread counts | List a user's conversations, Get one of a user's conversations |
For example, the latest 20 messages between alice and bob:
CONV=$(curl -s "$IM_API/$ORG/$APP/single-conversations/alice/bob" \
-H "Authorization: Bearer $APP_TOKEN" | jq -r .conversation_id)
curl "$IM_API/$ORG/$APP/conversations/$CONV/messages?limit=20" \
-H "Authorization: Bearer $APP_TOKEN"These APIs have two points of view:
| View | Returns | Messages seen |
|---|---|---|
| The server | Conversation objects and history | Every message in the conversation, unaffected by users deleting messages, clearing chat history, or when they joined |
| One user | User conversation objects: their read position, unread count, pin, whether they deleted the conversation, and their role in the group | Only what that user can see; everyone's state is independent |
A few basics:
- Two users have exactly one one-to-one conversation, created when they first exchange a message; a group's conversation is created with the group, and its ID is the group ID. See Conversation ID.
- Each message has a sequence
seqin its conversation, counting up from 1, the same for both sides of a one-to-one chat. Paging, read positions and the point where chat history was cleared are all sequences. - Messages past retention (90 days by default) are no longer returned; the conversation and users' conversation state aren't deleted with them.
Get conversation info
Get a conversation's basic info by conversation ID, for one-to-one and group conversations.
/{org_name}/{app_name}/conversations/{conversation_id}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
conversation_id | String | The conversation ID; for groups, the group ID |
Request example
curl "$IM_API/$ORG/$APP/conversations/99582660229201920" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK with a conversation object.
{
"conversation_id": "99582660229201920",
"conversation_type": "single",
"users": ["alice", "bob"],
"group_id": null,
"max_seq": 20,
"expired_seq": 0,
"message_version": 0,
"last_message_at": "2026-10-02T19:14:53.302Z",
"status": "active",
"created_at": "2026-10-02T19:05:56.355Z"
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The conversation doesn't exist, or conversation_id isn't a valid ID |
Get the one-to-one conversation between two users
Get the one-to-one conversation between two usernames as a conversation object. Use its conversation_id to fetch history.
/{org_name}/{app_name}/single-conversations/{username}/{peer}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | One user's username, case-insensitive |
peer | String | The other user's username, case-insensitive. The order of the two doesn't matter |
Request example
curl "$IM_API/$ORG/$APP/single-conversations/bob/alice" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK with a conversation object. The order of users is fixed and doesn't depend on the order in the path.
{
"conversation_id": "99582660229201920",
"conversation_type": "single",
"users": ["alice", "bob"],
"group_id": null,
"max_seq": 3,
"expired_seq": 0,
"message_version": 0,
"last_message_at": "2026-10-02T19:05:56.454Z",
"status": "active",
"created_at": "2026-10-02T19:05:56.355Z"
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | message "User not found": either user doesn't exist or was deleted |
| 404 | not_found | message "Conversation not found": the two have never exchanged a message, or username and peer are the same user |
Fetch a conversation's history
Fetch messages in a conversation by sequence or time range. This is the server's view: every message in the conversation, including ones a user deleted or cleared and ones from before a member joined. Recalled messages are returned without content, see Message object.
/{org_name}/{app_name}/conversations/{conversation_id}/messagesPath parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
conversation_id | String | The conversation ID |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
before_seq | Number | No | Messages with a sequence below it, for paging backward (older) |
after_seq | Number | No | Messages with a sequence above it, for paging forward (newer). 0 starts from the first message |
around_seq | Number | No | That message plus the ones around it, for viewing context from a message such as a search result |
start_time | String | No | Only messages at or after this time, in RFC 3339, such as 2026-10-02T19:07:00Z |
end_time | String | No | Only messages before this time, in RFC 3339 |
limit | Number | No | At most this many messages, default 20, 1 to 100 |
- Give at most one of
before_seq,after_seqandaround_seq; with none, you get the newest page. Sequences must be non-negative integers. start_timeandend_timecan be combined with a sequence parameter to restrict the time range, see Fetch by time range below.
Paging
In every case, items is in ascending sequence order.
| Request | Returns | Next page |
|---|---|---|
| No sequence parameter | The newest limit messages | Pass next_before_seq from the response as before_seq to page backward |
before_seq | The limit messages just below it | Same as above; has_more false means you've reached the earliest message |
after_seq | The limit messages just above it | Pass next_after_seq as after_seq to page forward; has_more false means you've reached the newest |
around_seq | The message itself, limit / 2 (rounded down) before it, and the rest after | Page backward with next_before_seq and forward with next_after_seq; has_more_before and has_more_after say whether each direction has more |
A page may have fewer than limit messages (for example when messages past retention are skipped, or the page ends early at 1 MB), so check has_more (has_more_before and has_more_after with around_seq) rather than the count.
Fetch by time range
| Parameters | Returns | Next page |
|---|---|---|
With start_time | From the start of the range in ascending sequence order, with next_after_seq and has_more | Keep start_time and end_time, add after_seq (the previous page's next_after_seq) |
Only end_time | The newest page before end_time | Keep end_time, add before_seq |
start_time is inclusive and end_time exclusive. Times can include a time zone, such as 2026-10-03T03:07:00+08:00 (encode + as %2B in a URL).
For example, export one one-to-one conversation's messages for October 2 (UTC):
# 第一页
curl "$IM_API/$ORG/$APP/conversations/99582660229201920/messages?start_time=2026-10-02T00:00:00Z&end_time=2026-10-03T00:00:00Z&limit=100" \
-H "Authorization: Bearer $APP_TOKEN"
# 之后的页:带上上一页的 next_after_seq,直到 has_more 为 false
curl "$IM_API/$ORG/$APP/conversations/99582660229201920/messages?start_time=2026-10-02T00:00:00Z&end_time=2026-10-03T00:00:00Z&limit=100&after_seq=100" \
-H "Authorization: Bearer $APP_TOKEN"Request example
Page backward for the 2 messages below sequence 4:
curl "$IM_API/$ORG/$APP/conversations/99582660229201920/messages?before_seq=4&limit=2" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK:
| Field | Type | Description |
|---|---|---|
items | Array<Object> | Message objects in ascending sequence order; empty if there are none |
next_before_seq | Number | Returned with before_seq or around_seq, or when neither a sequence parameter nor start_time is given. The lowest sequence scanned on this page; use it as before_seq for the next page |
next_after_seq | Number | Returned with after_seq or around_seq, or with start_time and no sequence parameter. The highest sequence scanned; use it as after_seq for the next page |
has_more | Boolean | Whether this direction has more. Not returned with around_seq, which uses the two fields below |
has_more_before | Boolean | Only with around_seq: whether there are older messages |
has_more_after | Boolean | Only with around_seq: whether there are newer messages |
{
"has_more": true,
"items": [
{
"conversation_id": "99582660229201920",
"conversation_type": "single",
"seq": 2,
"message_id": "99582660455694336",
"client_msg_id": "98a76014c1d7236246907d80fd19133e",
"sender": "bob",
"sender_type": "user",
"recipient": "alice",
"type": "text",
"body": { "text": "你好 Alice" },
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:05:56.408Z",
"message_version": 0
},
{
"conversation_id": "99582660229201920",
"conversation_type": "single",
"seq": 3,
"message_id": "99582660644438016",
"client_msg_id": "3a72c5d2992928f8ab91d233028878ee",
"sender": "alice",
"sender_type": "user",
"recipient": "bob",
"type": "text",
"body": { "text": "明天上午十点开会" },
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:05:56.454Z",
"message_version": 0
}
],
"next_before_seq": 2
}Fetching around message 5 with around_seq=5&limit=3 returns messages 4, 5 and 6 in items, with next_before_seq: 4, has_more_before: true, next_after_seq: 6 and has_more_after: true; continue with before_seq=4 and after_seq=6 in each direction.
Requests out of range (such as after_seq at or above the newest sequence, or before_seq of 1) return empty items with has_more false.
Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | More than one of before_seq, after_seq and around_seq; details.reason is conflicting_params |
| 400 | invalid_argument | A sequence parameter isn't a non-negative integer, limit isn't between 1 and 100, or start_time or end_time isn't RFC 3339 |
| 404 | not_found | The conversation doesn't exist |
List a user's conversations
List all of a user's conversation states: one-to-one chats they've had messages in, groups they're in, and groups they've left but can still read. Each item is a user conversation object with unread count, read position, pin and deletion state.
/{org_name}/{app_name}/users/{username}/conversationsIt differs from the conversation list clients show in three ways:
| Difference | What to do |
|---|---|
| Sorted by conversation ID, not by last message time or pins | Fetch every page, then sort yourself by pinned_at (pinned first) and last_message_at |
Includes conversations the user deleted (hidden true) and groups they left (membership left, removed or dismissed) | Skip the ones with hidden true when displaying |
| A newly joined group appears only once joining is processed (usually within seconds) | Until then, get the user's conversation by group ID |
Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username, case-insensitive |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | String | No | Paging cursor; omit for the first page, see Paging |
limit | Number | No | Page size, default 20, 1 to 100 |
Request example
curl "$IM_API/$ORG/$APP/users/bob/conversations?limit=2" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK: items is an array of user conversation objects and next_cursor is the next page's cursor, null when there's none. When the last page is exactly limit items, next_cursor may still be set and the page after it is empty.
{
"items": [
{
"conversation_id": "99582660229201920",
"conversation_type": "single",
"peer": "alice",
"group_id": null,
"membership": null,
"start_seq": 1,
"max_seq": 11,
"read_seq": 11,
"unread_count": 0,
"marked_unread": true,
"peer_read_seq": 10,
"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:09:19.677Z",
"version": 18
},
{
"conversation_id": "99582677887221760",
"conversation_type": "group",
"peer": null,
"group_id": "99582677887221760",
"membership": "member",
"start_seq": 1,
"max_seq": 5,
"read_seq": 0,
"unread_count": 3,
"marked_unread": false,
"peer_read_seq": null,
"mention_seq": null,
"cleared_seq": 0,
"hidden_seq": 0,
"hidden": false,
"pinned_at": "2026-10-02T19:09:44.467Z",
"joined_at": "2026-10-02T19:06:00.566Z",
"left_at": null,
"message_version": 0,
"last_message_at": "2026-10-02T19:06:06.310Z",
"version": 17
}
],
"next_cursor": "99582677887221760"
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | cursor is invalid, or limit isn't between 1 and 100 |
| 404 | not_found | The user doesn't exist or was deleted |
Get one of a user's conversations
Get a user's state in one conversation as a user conversation object: their unread count, read position, whether they were mentioned, and whether they pinned or deleted it.
/{org_name}/{app_name}/users/{username}/conversations/{conversation_id}The user must have access to the conversation: one side of a one-to-one chat, or a current or former member of a group (people who left, were removed or whose group was dissolved can still read messages from before they left).
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 example
curl "$IM_API/$ORG/$APP/users/carol/conversations/99582677887221760" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK with a user conversation object. Here carol has 3 unread messages, and message 5 mentioned her:
{
"conversation_id": "99582677887221760",
"conversation_type": "group",
"peer": null,
"group_id": "99582677887221760",
"membership": "member",
"start_seq": 1,
"max_seq": 5,
"read_seq": 0,
"unread_count": 3,
"marked_unread": false,
"peer_read_seq": null,
"mention_seq": 5,
"cleared_seq": 0,
"hidden_seq": 0,
"hidden": false,
"pinned_at": null,
"joined_at": "2026-10-02T19:06:00.566Z",
"left_at": null,
"message_version": 0,
"last_message_at": "2026-10-02T19:06:06.310Z",
"version": 1
}Errors
| HTTP status | code | Description |
|---|---|---|
| 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 leaving hasn't finished processing; retry shortly to get the left membership state |
Data structures
Conversation object
Conversation info from the server's view, returned by Get conversation info and Get the one-to-one conversation between two users.
| Field | Type | Description |
|---|---|---|
conversation_id | String | The conversation ID; for groups, the group ID |
conversation_type | String | single one-to-one / group group chat |
users | Array<String> | The two usernames of a one-to-one chat in a fixed order; null for deleted users. null for groups |
group_id | String | The group ID for groups; null for one-to-one |
max_seq | Number | The sequence of the last message |
expired_seq | Number | Messages up to this sequence passed retention and were deleted; 0 means none |
message_version | Number | Change version of existing messages: incremented when a message is recalled, edited, erased, reacted to or pinned |
last_message_at | String | Time of the last message; may be null |
status | String | active / ended (the group was dissolved and no new messages are written) |
created_at | String | When the conversation was created, i.e. when its first message was written |
{
"conversation_id": "99582864340811776",
"conversation_type": "group",
"users": null,
"group_id": "99582864340811776",
"max_seq": 3,
"expired_seq": 0,
"message_version": 0,
"last_message_at": "2026-10-02T19:14:07.880Z",
"status": "ended",
"created_at": "2026-10-02T19:06:45.376Z"
}User conversation object
A user's personal state in one conversation, returned by List a user's conversations, Get one of a user's conversations, and operations like Mark as read and Delete a conversation. Sequences and unread counts are computed over what this user can see.
| Field | Type | Description |
|---|---|---|
conversation_id | String | The conversation ID |
conversation_type | String | single one-to-one / group group chat |
peer | String | The other user in a one-to-one chat, null if deleted; null for groups |
group_id | String | The group ID for groups; null for one-to-one |
membership | String | The user's status in a group: member / left / removed / dismissed (the group was dissolved). null for one-to-one |
start_seq | Number | The first sequence the user can see. In groups it starts from their latest join; after clearing history it's the one after the clear point; expired messages are accounted for. Greater than max_seq means nothing is visible |
max_seq | Number | The last sequence the user can see. For a group they left, the last one they could see when leaving (usually the leaving notice) |
read_seq | Number | Read position: messages up to it are read. Only ever increases |
unread_count | Number | Unread count, see How unread counts work |
marked_unread | Boolean | Whether the user marked the conversation unread in a client (clients show an unread dot when the count is 0). Cleared by marking as read, clearing history or deleting the conversation |
peer_read_seq | Number | How far the other side of a one-to-one chat has read, for showing "Read"; null when one-to-one read receipts are off for the app. null for groups |
mention_seq | Number | The latest unread message that mentioned the user or everyone, for "You were mentioned"; null if none |
cleared_seq | Number | Where chat history was cleared; the user can't see messages up to it. 0 if never cleared |
hidden_seq | Number | Where the conversation was deleted; 0 if never deleted |
hidden | Boolean | Whether the conversation is deleted. When true, don't show it in the list; a new message sets it back to false, see Delete a conversation |
pinned_at | String | When the user pinned the conversation, null if not pinned. Users pin from clients, up to 100 each; deleting the conversation unpins it |
joined_at | String | When the user last joined the group; null for one-to-one |
left_at | String | When the user left the group, null while still a member; null for one-to-one |
message_version | Number | The conversation's current message change version, the same as message_version in the conversation object |
last_message_at | String | Time of the last message the user can see; may be null |
version | Number | The user's conversation-list version at this conversation's latest change. Client SDKs use it for incremental sync and servers usually don't need it; 0 in groups where no state has been written for the user yet |
A one-to-one example (bob's view: 10 messages, he's read up to 5, so 5 are unread; alice hasn't marked anything read yet):
{
"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
}A group the user was removed from (they can only see messages 7 to 9; 9 is the removal notice):
{
"conversation_id": "99582677887221760",
"conversation_type": "group",
"peer": null,
"group_id": "99582677887221760",
"membership": "removed",
"start_seq": 7,
"max_seq": 9,
"read_seq": 6,
"unread_count": 1,
"marked_unread": false,
"peer_read_seq": null,
"mention_seq": null,
"cleared_seq": 0,
"hidden_seq": 0,
"hidden": false,
"pinned_at": null,
"joined_at": "2026-10-02T19:12:43.955Z",
"left_at": "2026-10-02T19:13:17.866Z",
"message_version": 0,
"last_message_at": "2026-10-02T19:13:18.378Z",
"version": 1
}