Query and export messages
Read messages from your server.
| You want to | API |
|---|---|
| Archive or analyze: export every message in the app, then export incrementally | Export the app's messages |
| Look up one message by ID | Query a single message |
| Handle complaints or spam: list what one user sent | Query a user's sent messages |
| Read one conversation's history (optionally by time range) | Conversations and history |
They all return message objects from the server's view:
| Messages | Returned? |
|---|---|
| Deleted by users, or in cleared chat history | Yes, unaffected |
| Recalled or erased | Yes, but without content (body, ext and so on are null) |
| Past retention | No |
| Online-only messages | Not stored, so never found |
A page holds at most 1 MB of messages; past that the page ends early, and you keep paging with the paging fields as usual.
Export the app's messages
Export messages from every conversation in the app in ascending message ID order, including one-to-one, group messages and group notices.
/{org_name}/{app_name}/messagesLeave out after_message_id the first time to start from the earliest message; then pass the previous page's next_after_message_id each time until has_more is false. Save the last next_after_message_id and continue from it next time to export only new messages:
AFTER=$(cat last_message_id 2>/dev/null) # 上次导出到哪里;第一次为空
while :; do
PAGE=$(curl -s "$IM_API/$ORG/$APP/messages?limit=1000${AFTER:+&after_message_id=$AFTER}" \
-H "Authorization: Bearer $APP_TOKEN")
echo "$PAGE" | jq -c '.items[]' >> messages.jsonl
NEXT=$(echo "$PAGE" | jq -r '.next_after_message_id // empty')
[ -n "$NEXT" ] && AFTER=$NEXT
[ "$(echo "$PAGE" | jq -r '.has_more')" = true ] || break
done
echo "$AFTER" > last_message_id- Only messages older than 30 seconds: so paging by ID never skips anything, newly sent messages can be exported 30 seconds later.
- Exported copies aren't updated: messages recalled or edited after export keep their exported content. To keep content from before a recall, export before recalling.
- Filling gaps in message delivery callbacks: if you receive
message.sentthrough event callbacks, messages may be missed while the callback URL is paused, disabled or backed up. Export from a message ID before the gap to fill it, deduplicating bymessage_id; see Making the most of message callbacks.
Path parameters
For org_name and app_name, see the server overview.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
after_message_id | String | No | Export messages after this ID (not including it); pass the previous page's next_after_message_id. Omit to start from the earliest message |
limit | Number | No | Page size, default 100, 1 to 1000 |
Request example
curl "$IM_API/$ORG/$APP/messages?after_message_id=99582642738954240&limit=2" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK:
| Field | Type | Description |
|---|---|---|
items | Array<Object> | This page's message objects, in ascending message ID order |
next_after_message_id | String | Where the next page starts; pass it as after_message_id. Equal to the request's after_message_id when the page is empty |
has_more | Boolean | Whether there may be another page. When true, keep requesting; the next page may be empty |
{
"items": [
{
"conversation_id": "99582604130385920",
"conversation_type": "single",
"seq": 7,
"message_id": "99582642919309312",
"client_msg_id": "2ec322dc5f952fd2d05bc6df94a44d9a",
"sender": "bob",
"sender_type": "user",
"recipient": "alice",
"type": "custom",
"body": { "card": "order_shipped", "order_id": "8812", "status": "delivered" },
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": { "at": "2026-10-02T19:08:27.418Z", "count": 2 },
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:05:52.228Z",
"message_version": 2
},
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 3,
"message_id": "99582672556261376",
"client_msg_id": "doc-grp-0001",
"sender": "alice",
"sender_type": "user",
"recipient": null,
"type": "text",
"body": { "text": "@bob 请看一下需求文档" },
"ext": null,
"mentions": { "usernames": ["bob"], "all": false },
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": { "by": null, "at": "2026-10-02T19:08:36.382Z" },
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:05:59.294Z",
"message_version": 1
}
],
"next_after_message_id": "99582672556261376",
"has_more": true
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | after_message_id isn't a non-negative integer, or limit isn't between 1 and 1000 |
Query a single message
Look up one message by message ID.
/{org_name}/{app_name}/messages/{message_id}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
message_id | String | The message ID |
Request example
curl "$IM_API/$ORG/$APP/messages/99584445836689408" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK with a message object:
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 9,
"message_id": "99584445836689408",
"client_msg_id": "5f0c2a7e-91b4-4d2e-a0c3-6b1e8f2d4c19",
"sender": "bob",
"sender_type": "user",
"recipient": null,
"type": "text",
"body": { "text": "@carol 文档已更新,请确认" },
"ext": { "biz_id": "PRD-2041" },
"mentions": { "usernames": ["carol"], "all": false },
"reply_to": { "seq": 3, "sender": "alice" },
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:13:02.076Z",
"message_version": 0
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The message doesn't exist, is past retention, or message_id is malformed |
Query a user's sent messages
List messages a user sent, newest first (one-to-one and group, whether sent from a client or by your server as them), optionally within a time range. Often used for complaints and spam: find what someone sent during a period, then recall them in a batch.
/{org_name}/{app_name}/users/{username}/sent-messagesUses cursor paging:
- A page may have fewer than
limititems (messages past retention are skipped); check whethernext_cursorisnullto know if there's another page. - Keep
start_timeandend_timethe same while paging.
Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The sender's username |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
start_time | String | No | Only messages sent at or after this time, in RFC 3339, such as 2026-10-02T00:00:00Z. With a time zone offset, encode + as %2B, such as 2026-10-02T08:00:00%2B08:00 |
end_time | String | No | Only messages sent before this time (exclusive), same format |
cursor | String | No | The cursor for the next page; omit for the first page |
limit | Number | No | Page size, default 20, 1 to 100 |
Request example
curl "$IM_API/$ORG/$APP/users/bob/sent-messages?start_time=2026-10-02T00:00:00Z&limit=2" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK, with a list of message objects in items and the next page's cursor in next_cursor:
{
"items": [
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 9,
"message_id": "99584445836689408",
"client_msg_id": "5f0c2a7e-91b4-4d2e-a0c3-6b1e8f2d4c19",
"sender": "bob",
"sender_type": "user",
"recipient": null,
"type": "text",
"body": { "text": "@carol 文档已更新,请确认" },
"ext": { "biz_id": "PRD-2041" },
"mentions": { "usernames": ["carol"], "all": false },
"reply_to": { "seq": 3, "sender": "alice" },
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:13:02.076Z",
"message_version": 0
},
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 7,
"message_id": "99582995840630784",
"client_msg_id": "97250496646553a053befca0da838392",
"sender": "bob",
"sender_type": "user",
"recipient": null,
"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:07:16.371Z",
"message_version": 0
}
],
"next_cursor": "99582995840630784"
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | start_time or end_time isn't RFC 3339 (with an offset, + must be written %2B), cursor is invalid, or limit isn't between 1 and 100 |
| 404 | not_found | The user doesn't exist or was deleted |
