English | 简体中文
WeFlow provides a local HTTP API (GET and POST are supported) so that external scripts and tools can read chat messages, sessions, contacts, group members and exported media files. It can also push message events proactively over a fixed SSE address when new messages are detected.
Enable API Service on the application's settings page.
- Default listen address:
127.0.0.1 - Default port:
5031 - Base URL:
http://127.0.0.1:5031 - Optionally enable
Active push: when a newly received message is detected it is pushed to SSE subscribers viaGET /api/v1/push/messages
State persistence: the state and port of the API service and active push are saved automatically and restored when WeFlow restarts.
Access Token: every /api/v1/* endpoint except the health check is protected by a token. Three ways to pass it (pick one):
- HTTP header (recommended):
Authorization: Bearer <your token> - Query parameter:
?access_token=<your token>(recommended for long-lived SSE connections) - JSON body:
{"access_token": "<your token>"}(POST requests only)
GET|POST /healthGET|POST /api/v1/healthGET|POST /api/v1/push/messagesGET|POST /api/v1/messagesGET|POST /api/v1/sessionsGET /api/v1/sessions/:id/messages(ChatLab Pull)GET|POST /api/v1/contactsGET|POST /api/v1/group-membersGET|POST /api/v1/media/*
Request
GET /healthor
GET /api/v1/healthResponse
{
"status": "ok"
}Receive new-message events over a long-lived SSE connection. It shares the port with the HTTP API.
Request
GET /api/v1/push/messagesHTTP API servicemust be enabled on the settings page firstActive pushmust be enabled as well- The response type is
text/event-stream - Event names are
message.newandmessage.revoke - Receivers should de-duplicate by
event + rawid
eventsessionIdrawidavatarUrlsourceNamegroupName(group chats only)contenttimestamp(message time, Unix timestamp in seconds)
curl -N "http://127.0.0.1:5031/api/v1/push/messages?access_token=YOUR_TOKEN"Example event:
event: message.new
data: {"event":"message.new","sessionId":"xxx@chatroom","sessionType":"group","rawid":"1234567890123456789","avatarUrl":"https://example.com/group.jpg","sourceName":"Li Si","groupName":"Project group","content":"[Image]","timestamp":1760000123}
Example revoke event:
event: message.revoke
data: {"event":"message.revoke","sessionId":"wxid_xxx","sessionType":"other","rawid":"1234567890123456789","avatarUrl":"https://example.com/avatar.jpg","sourceName":"Zhang San","content":"The other party recalled a message (rawid: 1234567890123456789), content: \"Hello\"","timestamp":1760000180}
With POST, put the parameters in the JSON body (Content-Type: application/json)
Reads the messages of a session; raw JSON and ChatLab formats are supported.
Request
GET /api/v1/messages| Parameter | Type | Required | Description |
|---|---|---|---|
talker |
string | Yes | Session ID. For a private chat usually the other party's wxid; for a group xxx@chatroom |
limit |
number | No | Number of messages, default 100, range 1~10000 |
offset |
number | No | Pagination offset, default 0 |
start |
string | No | Start time, YYYYMMDD or a timestamp |
end |
string | No | End time, YYYYMMDD or a timestamp |
keyword |
string | No | Filter on the message display text |
chatlab |
string | No | 1/true outputs ChatLab format |
format |
string | No | json or chatlab |
media |
string | No | 1/true exports media and returns media URLs; alias meiti |
image |
string | No | With media=1, controls image export; alias tupian |
voice |
string | No | With media=1, controls voice export; alias vioce |
video |
string | No | With media=1, controls video export |
emoji |
string | No | With media=1, controls sticker export |
curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&limit=20"
curl "http://127.0.0.1:5031/api/v1/messages?talker=xxx@chatroom&chatlab=1"
curl "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx&start=20260101&end=20260131"
curl "http://127.0.0.1:5031/api/v1/messages?talker=xxx@chatroom&media=1&image=1&voice=0&video=0&emoji=0"Top level:
successtalkercounthasMoremedia.enabledmedia.exportPathmedia.countmessages
Per message:
localIdserverIdlocalTypecreateTimeisSendsenderUsernamecontentrawContentparsedContentreplyToMessageId(theserverIdof the message being replied to; quote messages only)quote(snapshot of the quoted message: its ID, sender, content and type)mediaTypemediaFileNamemediaUrlmediaLocalPath
Example response
{
"success": true,
"talker": "xxx@chatroom",
"count": 3,
"hasMore": true,
"media": {
"enabled": true,
"exportPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media",
"count": 1
},
"messages": [
{
"localId": 123,
"serverId": "6116895530414915131",
"localType": 1,
"createTime": 1738713600,
"isSend": 0,
"senderUsername": "wxid_member",
"content": "Hello",
"rawContent": "Hello",
"parsedContent": "Hello"
},
{
"localId": 125,
"serverId": "6116895530414915133",
"localType": 244813135921,
"createTime": 1738713700,
"isSend": 0,
"senderUsername": "wxid_member",
"content": "Got it",
"rawContent": "<msg>...</msg>",
"parsedContent": "Got it",
"replyToMessageId": "6116895530414915131",
"quote": {
"platformMessageId": "6116895530414915131",
"sender": "wxid_other",
"accountName": "Zhang San",
"content": "Hello",
"type": 0
}
},
{
"localId": 124,
"localType": 3,
"createTime": 1738713660,
"isSend": 0,
"senderUsername": "wxid_member",
"content": "[Image]",
"mediaType": "image",
"mediaFileName": "abc123.jpg",
"mediaUrl": "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/images/abc123.jpg",
"mediaLocalPath": "C:\\Users\\Alice\\Documents\\WeFlow\\api-media\\xxx@chatroom\\images\\abc123.jpg"
}
]
}With chatlab=1 or format=chatlab a ChatLab structure is returned:
chatlab.versionchatlab.exportedAtchatlab.generatormeta.namemeta.platformmeta.typemeta.groupIdmeta.groupAvatarmeta.ownerIdmembers[].platformIdmembers[].accountNamemembers[].groupNicknamemembers[].avatarmessages[].sendermessages[].accountNamemessages[].groupNicknamemessages[].timestampmessages[].typemessages[].contentmessages[].platformMessageIdmessages[].replyToMessageIdmessages[].mediaPath
In group chats groupNickname comes from the member's group nickname first; if the source data lacks it, it falls back to empty or the display name.
With POST, put the parameters in the JSON body (Content-Type: application/json)
Request
GET /api/v1/sessions| Parameter | Type | Required | Description |
|---|---|---|---|
keyword |
string | No | Matches username or displayName |
limit |
number | No | Default 100 |
successcountsessions[].usernamesessions[].displayNamesessions[].typesessions[].lastTimestampsessions[].unreadCount
Example response
{
"success": true,
"count": 1,
"sessions": [
{
"username": "xxx@chatroom",
"displayName": "Project group",
"type": 2,
"lastTimestamp": 1738713600,
"unreadCount": 0
}
]
}With format=chatlab the response follows the ChatLab Pull protocol and can be used directly as a ChatLab remote data source.
Request
GET /api/v1/sessions?format=chatlab| Parameter | Type | Required | Description |
|---|---|---|---|
format |
string | Yes | Set to chatlab |
keyword |
string | No | Matches username or displayName |
limit |
number | No | Default 100 |
{
"sessions": [
{
"id": "xxx@chatroom",
"name": "Project group",
"platform": "wechat",
"type": "group",
"messageCount": 58000,
"lastMessageAt": 1738713600
}
]
}| Field | Description |
|---|---|
id |
Session ID (WeChat username) |
name |
Session display name |
platform |
Always wechat |
type |
group (group chat) or private (private chat) |
messageCount |
Message count (an estimate, may be inexact) |
lastMessageAt |
Unix timestamp (seconds) of the last message |
Returns chat data in the standard ChatLab format, with incremental pulls and pagination.
Request
GET /api/v1/sessions/:id/messages| Parameter | Type | Required | Description |
|---|---|---|---|
:id |
string | Yes | Session ID (path parameter) |
since |
number | No | Unix timestamp (seconds); only messages after it are returned |
end |
number | No | Unix timestamp (seconds), upper time bound |
limit |
number | No | Per-call limit, default and maximum 5000 |
offset |
number | No | Pagination offset, default 0 |
Standard ChatLab JSON plus a sync pagination block:
{
"chatlab": {
"version": "0.0.2",
"exportedAt": 1738713600,
"generator": "WeFlow"
},
"meta": {
"name": "Project group",
"platform": "wechat",
"type": "group",
"groupId": "xxx@chatroom",
"ownerId": "wxid_xxx"
},
"members": [
{
"platformId": "wxid_a",
"accountName": "Zhang San",
"groupNickname": "Product",
"avatar": "https://example.com/avatar.jpg"
}
],
"messages": [
{
"sender": "wxid_a",
"accountName": "Zhang San",
"timestamp": 1738713600,
"type": 0,
"content": "Hello",
"platformMessageId": "123456"
}
],
"sync": {
"hasMore": true,
"nextSince": 1738713600,
"nextOffset": 5000,
"watermark": 1738714000
}
}| Field | Description |
|---|---|
hasMore |
Whether more data is available |
nextSince |
since value for the next request |
nextOffset |
offset value for the next request |
watermark |
Upper time bound of this pull (seconds timestamp) |
Connecting ChatLab: in ChatLab's settings add a remote data source, set baseUrl to http://127.0.0.1:5031/api/v1 and the token to the API token configured in WeFlow.
With POST, put the parameters in the JSON body (Content-Type: application/json)
Request
GET /api/v1/contacts| Parameter | Type | Required | Description |
|---|---|---|---|
keyword |
string | No | Matches username, nickname, remark, displayName |
limit |
number | No | Default 100 |
successcountcontacts[].usernamecontacts[].displayNamecontacts[].remarkcontacts[].nicknamecontacts[].aliascontacts[].avatarUrlcontacts[].type
Example response
{
"success": true,
"count": 1,
"contacts": [
{
"username": "wxid_xxx",
"displayName": "Zhang San",
"remark": "Client Zhang San",
"nickname": "Zhang San",
"alias": "zhangsan",
"avatarUrl": "https://example.com/avatar.jpg",
"type": "friend"
}
]
}With POST, put the parameters in the JSON body (Content-Type: application/json)
Returns each group member's wxid, group nickname, remark, WeChat ID and more.
Request
GET /api/v1/group-members| Parameter | Type | Required | Description |
|---|---|---|---|
chatroomId |
string | Yes | Group ID; talker is accepted as well |
includeMessageCounts |
string | No | 1/true adds each member's message count |
withCounts |
string | No | Alias of includeMessageCounts |
forceRefresh |
string | No | 1/true bypasses the in-memory cache and refreshes |
successchatroomIdcountfromCacheupdatedAtmembers[].wxidmembers[].displayNamemembers[].nicknamemembers[].remarkmembers[].aliasmembers[].groupNicknamemembers[].avatarUrlmembers[].isOwnermembers[].isFriendmembers[].messageCount
Example requests
curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom"
curl "http://127.0.0.1:5031/api/v1/group-members?chatroomId=xxx@chatroom&includeMessageCounts=1&forceRefresh=1"Example response
{
"success": true,
"chatroomId": "xxx@chatroom",
"count": 2,
"fromCache": false,
"updatedAt": 1760000000000,
"members": [
{
"wxid": "wxid_member_a",
"displayName": "Client A",
"nickname": "Jia",
"remark": "Client A",
"alias": "kehua",
"groupNickname": "Party A",
"avatarUrl": "https://example.com/a.jpg",
"isOwner": true,
"isFriend": true,
"messageCount": 128
},
{
"wxid": "wxid_member_b",
"displayName": "Li Si",
"nickname": "Li Si",
"remark": "",
"alias": "",
"groupNickname": "",
"avatarUrl": "",
"isOwner": false,
"isFriend": false,
"messageCount": 0
}
]
}Notes:
displayNameis the primary display name inside the application.groupNicknameis the member's nickname in that group.remarkis the remark you gave the contact.aliasis the WeChat ID.groupNicknameis empty when the WeChat source data has no group nickname.
GET /api/v1/sns/timelineParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
limit |
number | No | Number of posts, default 20, range 1~200 |
offset |
number | No | Offset, default 0 |
usernames |
string | No | Filter by poster, comma separated, e.g. wxid_a,wxid_b |
keyword |
string | No | Keyword filter (post text) |
start |
string | No | Start time, YYYYMMDD or a seconds/milliseconds timestamp |
end |
string | No | End time, YYYYMMDD or a seconds/milliseconds timestamp |
media |
number | No | Whether to return directly accessible media URLs, default 1 |
replace |
number | No | With media=1, whether resolved URLs overwrite media.url/thumb, default 1 |
inline |
number | No | With media=1, whether to return inline data: URLs, default 0 |
Examples:
curl "http://127.0.0.1:5031/api/v1/sns/timeline?limit=5"
curl "http://127.0.0.1:5031/api/v1/sns/timeline?usernames=wxid_a,wxid_b&keyword=travel"
curl "http://127.0.0.1:5031/api/v1/sns/timeline?limit=3&media=1&replace=1"
curl "http://127.0.0.1:5031/api/v1/sns/timeline?limit=3&media=1&inline=1"Media fields (media=1):
media[].url/thumb: the fields you should normally use directly.- With
replace=1(default),media[].url/thumbare replaced with accessible URLs, equivalent toresolvedUrl/resolvedThumbUrl. - With
replace=0,media[].url/thumbkeep the original WeChat URLs; combine them with theraw/proxy/resolvedfields below to choose for yourself. media[].rawUrl/rawThumb: the original Moments URLsmedia[].proxyUrl/proxyThumbUrl: directly accessible proxy URLsmedia[].resolvedUrl/resolvedThumbUrl: the final usable URLs (may bedata:URLs wheninline=1)media[].token/key/encIdx: access/decryption parameters from the WeChat source data. You usually do not need to handle them; if you call/api/v1/sns/media/proxyby hand, pass the current entry'surlandkeyback unchanged.media[].livePhoto: the video part of a Live Photo. The outermedia[].url/thumbis still the cover image;livePhotoprovides its own set ofurl/thumb/raw*/proxy*/resolved*fields.- With
media=0,raw*/proxy*/resolved*are not added; the endpoint returns only the originalurl/thumband the source fields (such askey/token/encIdx).
GET /api/v1/sns/usernamesGET /api/v1/sns/export/statsParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
fast |
number | No | 1 uses fast statistics (cache first) |
GET /api/v1/sns/media/proxyParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | Original media URL |
key |
string/number | No | Decryption key (needed by some resources) |
POST /api/v1/sns/export
Content-Type: application/jsonExample body:
{
"outputDir": "C:\\Users\\Alice\\Desktop\\sns-export",
"format": "json",
"usernames": "wxid_a,wxid_b",
"keyword": "travel",
"exportMedia": true,
"exportImages": true,
"exportLivePhotos": true,
"exportVideos": true,
"start": "20250101",
"end": "20251231"
}format supports json, html and arkmejson (also written arkme-json).
GET /api/v1/sns/block-delete/status
POST /api/v1/sns/block-delete/install
POST /api/v1/sns/block-delete/uninstallDELETE /api/v1/sns/post/{postId}With POST, put the parameters in the JSON body (Content-Type: application/json)
After media=1 is enabled on the message endpoint, images, voice, video and stickers are first exported to a local cache directory, and accessible HTTP URLs are returned.
Request
GET /api/v1/media/{relativePath}curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/images/abc123.jpg"
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/voices/voice_100.wav"
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/videos/video_200.mp4"
curl "http://127.0.0.1:5031/api/v1/media/xxx@chatroom/emojis/emoji_300.gif"| Extension | Content-Type |
|---|---|
.png |
image/png |
.jpg / .jpeg |
image/jpeg |
.gif |
image/gif |
.webp |
image/webp |
.wav |
audio/wav |
.mp3 |
audio/mpeg |
.mp4 |
video/mp4 |
Common error response:
{
"error": "Media not found"
}$headers = @{ "Authorization" = "Bearer YOUR_TOKEN" }
$body = @{ talker = "wxid_xxx"; limit = 10 } | ConvertTo-Json
Invoke-RestMethod -Uri "http://127.0.0.1:5031/api/v1/messages" -Method POST -Headers $headers -Body $body -ContentType "application/json"# GET with a token header
curl -H "Authorization: Bearer YOUR_TOKEN" "http://127.0.0.1:5031/api/v1/messages?talker=wxid_xxx"
# POST with a JSON body
curl -X POST http://127.0.0.1:5031/api/v1/messages \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"talker": "xxx@chatroom", "chatlab": true}'import requests
BASE_URL = "http://127.0.0.1:5031"
headers = {"Authorization": "Bearer YOUR_TOKEN", "Content-Type": "application/json"}
# Get messages with POST
messages = requests.post(
f"{BASE_URL}/api/v1/messages",
json={"talker": "xxx@chatroom", "limit": 50},
headers=headers
).json()
# Get group members with GET
members = requests.get(
f"{BASE_URL}/api/v1/group-members",
params={"chatroomId": "xxx@chatroom", "includeMessageCounts": 1},
headers=headers
).json()- The API listens only on the local
127.0.0.1and is not exposed to the internet. - The database connection must be completed in WeFlow before use.
startandendacceptYYYYMMDDand timestamps; a plainYYYYMMDDendis extended to23:59:59of that day.- A group member's
groupNicknamedepends on the WeChat source data; it is not filled in automatically when missing. - Media links are only accessible after the corresponding messages have been exported with
media=1.