Skip to content

HTTP API

Everything the CLI and the OpenClaw plugin do goes through this API, so another agent framework, a bot or an app can do the same. The daemon serves JSON at http://127.0.0.1:6174/v1. Every request except GET /health needs a bearer token; create one with wx tokens create.

A typical integration follows the event stream for new messages, fetches each one, and replies through /sends.

Terminal window
export WX=http://127.0.0.1:6174/v1 TOKEN=... # and ACCOUNT=<id from /v1/accounts>
curl -H "Authorization: Bearer $TOKEN" $WX/accounts

For calls, use the separate Calls API reference for WebSocket commands, events and payload examples, and the media reference for PCM framing.

Profiles

Use the existing daemon connection key for profile management. There is no separate frontend role to configure.

ActionRequest
List profilesGET /accounts
Add a profilePOST /accounts with { "alias": "Personal" }
Sign inPOST /accounts/{id}/login-attempts with an Idempotency-Key
Sign outPOST /accounts/{id}/auth/logout with an Idempotency-Key
Remove a signed-out profileDELETE /accounts/{id} with { "confirmAccountId": "<same id>" }

Removal returns { "accountId": "…", "removed": true, "filesRetained": true }. It unregisters the profile and keeps its local files and recovery records. Sign out and wait for active operations to finish first; otherwise the response is 409. Removed IDs and profile paths stay reserved. The existing token scopes apply: creation, renaming and removal use admin; sign-in and sign-out use login.

Read

Terminal window
curl -H "Authorization: Bearer $TOKEN" "$WX/accounts/$ACCOUNT/threads?limit=20"
curl -H "Authorization: Bearer $TOKEN" "$WX/accounts/$ACCOUNT/threads/filehelper/messages?limit=50"
curl -H "Authorization: Bearer $TOKEN" "$WX/accounts/$ACCOUNT/messages?query=invoice"

Lists return { "items": [...], "nextCursor": "..." }. Pass cursor= with the same query to get the next page.

GET /threads and GET /threads/{id} include sidebar summaries from saved history:

FieldContents
mutedSaved notification preference for a direct chat or group: true, false, or null when missing or unresolved.
pinnedSaved pin state for an ordinary direct chat or active group: true, false, or null when unavailable.
lastMessageLatest local message ID, sender ID/name, direction, type, time and a plain-text text preview of up to 160 characters. null means empty local history; omitted means the preview is unavailable.
avatarMembersUp to four cached group members, each with id and displayName; empty for direct chats or an unavailable roster. Fetch cached pictures through GET /avatars/{username}.

These reads do not refresh WeChat or download media. Media messages use labels such as “Photo” and “Sticker”; recalled content stays suppressed. Refresh the thread list after message, contact or group events to update the summaries. Contact refresh and sync update saved metadata. Notification consumers should require account status contactSyncReady: true and thread muted: false before alerting. Notification edits through PUT /threads/{id}/notifications support ordinary direct contacts and groups. A direct edit reads the current contact before submitting its change. Repeat the same idempotency key to recover an operation; an uncertain outcome must not be resubmitted with a new key.

Pin or unpin with PUT /threads/{id}/pin, body {"pinned":true} or {"pinned":false}, an admin token and Idempotency-Key. The SDK method is setThreadPinned(accountId, threadId, pinned, key). The response is an operation; inspect its remoteOutcome and profileProjection. Saved pin changes emit thread.updated; refetch the thread or list. A new list places pinned chats first, then orders each section by saved activity. An existing pagination cursor keeps its original order.

Contacts

List, search and read contacts saved in one account’s WeChat profile. All routes below start with /v1/accounts/{account} and require read scope for that account. Choose ACCOUNT from GET /v1/accounts; it is the daemon’s account ID, not a contact’s WeChat username. Percent-encode usernames in URL paths.

TaskRequestTypeScript SDK
List or search saved contactsGET /contacts?search=Alice&limit=20contacts(accountId, { query: "Alice", limit: 20 })
Read one saved contactGET /contacts/{username}contact(accountId, username)
Refresh one known identityPOST /contacts/{username}/refresh with {}refreshContact(accountId, username)
Fetch the contact inventoryPOST /contact-sync with {}syncContacts(accountId)
Look up known identities on WeChatPOST /contacts/lookup with {"ids":["wxid_alice"]}No dedicated method

Read saved contacts

  1. Search by a case-insensitive substring of the ID, display name, nickname, remark or alias. query is an alias for search; use one, not both.

    Terminal window
    curl -G -H "Authorization: Bearer $TOKEN" \
    --data-urlencode 'search=Alice' --data-urlencode 'limit=20' \
    "$WX/accounts/$ACCOUNT/contacts" > contacts-page.json

    An abridged, synthetic response:

    {
    "items": [{
    "id": "wxid_alice", "username": "wxid_alice",
    "displayName": "Alice", "nickname": "Alice Chen",
    "remark": "Alice", "alias": null, "relation": "unknown",
    "classification": {
    "category": "unknown", "namespace": "wechat",
    "businessType": null, "businessKind": null, "openIm": null
    }
    }],
    "nextCursor": "OPAQUE_CURSOR",
    "sourceGeneration": "PROFILE_GENERATION",
    "asOfEventCursor": "EVENT_CURSOR"
    }
  2. If nextCursor is non-null, pass it with the same search. Repeat until null:

    Terminal window
    CURSOR=$(jq -r '.nextCursor' contacts-page.json)
    curl -G -H "Authorization: Bearer $TOKEN" \
    --data-urlencode 'search=Alice' --data-urlencode "cursor=$CURSOR" \
    --data-urlencode 'limit=20' "$WX/accounts/$ACCOUNT/contacts" > contacts-page.json

    The default limit is 50; accepted limits are 1–200. Search accepts at most 256 UTF-8 bytes. Cursors expire after 30 minutes and retain the first page’s IDs and order; fields reflect current saved data. Restart without a cursor after 409 pagination_cursor_expired. A removed row can become an {"id":"wxid_alice","deleted":true,"version":"..."} item.

  3. For a known username, use GET /contacts/wxid_alice. A missing saved contact returns 404 not_found; this does not establish friendship or server absence. CLI equivalents are wx --json contacts list --limit 20, wx --json contacts find Alice and wx --json contacts get wxid_alice.

id and username identify the same peer. displayName prefers a nonempty remark, then nickname, then username. nickname, remark and alias may be null. relation currently remains unknown, including for saved friends. Contacts exclude ordinary group records; use groups and member reads for rosters. This API does not add friends, delete contacts or edit their remarks.

Refresh and sync contacts

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{}' \
"$WX/accounts/$ACCOUNT/contacts/wxid_alice/refresh"

Refresh and inventory requests return HTTP 202 with an operation (abridged): {"id":"OPERATION_ID","kind":"metadata","state":"queued"}. Poll GET /operations/{id} or client.operation(accountId, op.id), then reread contacts. A finished operation can still have result.profileProjection set to "partial"; inspect the result rather than relying on its state alone.

A refresh retrieves one known identity and saves supported fields. Contact sync fetches the inventory in up to 10 pages or two minutes per operation. Inspect complete, catalogComplete, failedItems, unavailableCount and resumeAvailable in its result; call it again to continue saved progress. Unavailable contacts do not imply deletion. CLI equivalents are wx contacts refresh wxid_alice and wx contacts sync.

This inventory request is separate from the daemon’s automatic contact-change stream. That stream emits contacts.updated after supported changes are saved and contacts.sync for progress. Refetch affected resources after events. Account status contactSyncReady means that stream caught up in this connection; it does not promise a complete contact inventory.

POST /contacts/lookup is a different, immediate server lookup: it accepts one or two distinct known IDs, returns items plus numeric per-ID results, and reports profileProjection: "pending" on returned items. It does not save those contacts. Prefer refresh when you need subsequent cached reads to change.

Interpret peer classification

Contact, thread, group and member reads derive classification from the same saved profile. It is independent of thread kind and contact relation.

categorySaved evidence
businessOfficial/business account metadata.
openim / openim_customer_serviceWeCom contact / separate customer-service identity.
group / systemRecognized group / built-in peer such as File Transfer.
unknownNo established category; never assume a personal account or friend.

namespace distinguishes identifier families; it grants no additional actions. businessType retains the numeric value; businessKind is official_account, service_account, enterprise, enterprise_child, photo_account, or null for unknown/conflicting values. An Official Account label does not establish a subscription account. openIm contains cached appId, wordingId, localized wordings (languageId, text) and application/account-type references; choose the language in your app.

Send

Terminal window
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-confirmation" \
-d '{"threadId": "filehelper", "text": "Order 1234 confirmed"}' \
"$WX/accounts/$ACCOUNT/sends"

Every send needs an Idempotency-Key. Sending the same key again returns the original send instead of sending twice. Check progress with GET /sends/{id}, or find a send by key with GET /sends/by-idempotency-key?key=....

Group text accepts mentions: { userIds: ["MEMBER_ID"] } or mentions: { all: true }. See sending and receiving mentions for CLI, SDK and response examples.

To quote a saved message, send both its id and serverId from that chat’s message list:

{
"kind": "text",
"threadId": "filehelper",
"text": "Agreed",
"quote": { "messageId": "MESSAGE_ID", "serverId": "SERVER_ID" }
}

The token needs read and send. The daemon reads the original from the same chat; callers supply only its IDs. In the TypeScript SDK:

await client.sendText(accountId, threadId, "Agreed", key, { messageId, serverId });

To mention members in a quoted group reply, include both options:

await client.sendText(accountId, groupId, "Agreed", key, {
quote: { messageId, serverId },
mentions: { userIds: [memberId] },
});

The HTTP request likewise accepts quote and mentions together for group text.

See quoted replies in the CLI guide for supported originals and limits.

For files, images, video and voice, stage bytes with POST /uploads and send the returned source ID. Send attachments covers the request, SDK methods, per-kind capability limits and longer job polling.

Receive

Terminal window
curl -N -H "Authorization: Bearer $TOKEN" "$WX/accounts/$ACCOUNT/events/stream"

A server-sent event stream. A new message arrives as a message.committed event; fetch it with GET /messages/{messageId}. To resume after a disconnect, reconnect with ?after=CURSOR (or the standard Last-Event-ID header) and the daemon replays what you missed. POST /consumers/{name}/ack with {"cursor": "..."} records how far you’ve processed.

The stream sends an immediate heartbeat comment, followed by periodic keepalives. Heartbeat comments contain no message and do not advance the event cursor.

Recognized group changes emit group.updated after the saved profile changes; fetch GET /groups/{groupId} or its members. group.refresh_blocked means a follow-up read could not finish. These events share the same replayable stream. Automatic delivery of every edit made on another device is not established; see group refresh limits.

Routes

All account routes start with /v1/accounts/{account}.

AreaRoutes
AccountsGET /v1/accounts, GET /, GET /capabilities, POST /connect, POST /disconnect, POST /logout
LoginPOST /login-attempts, GET /login-attempts/{id}, GET /login-attempts/{id}/qr, POST /login-attempts/{id}/cancel
Chats and messagesGET /threads, GET /threads/{id}/messages, GET /messages?query=, GET /messages/{id}
ContactsGET /contacts, GET /contacts/{username}, POST /contacts/{username}/refresh, POST /contact-sync, POST /contacts/lookup
GroupsGET /groups, GET /groups/{id}, GET /groups/{id}/members, GET /groups/{id}/members/{username}
Group edits and chat settings (admin)POST /groups, PATCH /groups/{id}, POST /groups/{id}/leave, POST/DELETE /groups/{id}/members, PUT /groups/{id}/my-name, DELETE /groups/{id}/announcement, PUT /threads/{id}/notifications, PUT /threads/{id}/pin
SendingPOST /uploads, POST /sends, GET /sends/{id}, POST /sends/{id}/cancel, POST /messages/{id}/recall
MediaGET /messages/{id}/media, POST /media/lookup, POST /messages/{id}/media-transfers, GET /media/{id}/content
Read statePOST /threads/{id}/read-state with {"expectedVersion": "..."}
Background workGET /jobs/{id}, GET /operations/{id}
EventsGET /events, GET /events/stream, POST /consumers/{name}/ack
TokensGET /v1/tokens, POST /v1/tokens, DELETE /v1/tokens/{id}

Request and response types are generated from the Rust source into native.generated.ts. The wechat-api crate is the source of truth.

To read attachment metadata together, send {"messageIds":["MESSAGE_ID_1","MESSAGE_ID_2"]} to POST /media/lookup with the media scope. A batch accepts up to 40 message IDs and returns an items array with each messageId, its assets and an error code or null. It does not download attachments. In the SDK, use client.mediaAssetsBatch(accountId, messageIds).