website/docs/user-guide/messaging/wecom-callback.md
Connect Hermes to WeCom (Enterprise WeChat) as a self-built enterprise application using the callback/webhook model.
:::info WeCom Bot vs WeCom Callback Hermes supports two WeCom integration modes:
See also: WeCom Bot for the bot-style integration.
Run
hermes gateway setupand pick WeCom Callback for a guided walk-through.
message/send APIaiohttp and httpx Python packages (included in the default install)http://YOUR_PUBLIC_IP:8645/wecom/callbackAdd to your .env file:
WECOM_CALLBACK_CORP_ID=your-corp-id
WECOM_CALLBACK_CORP_SECRET=your-corp-secret
WECOM_CALLBACK_AGENT_ID=1000002
WECOM_CALLBACK_TOKEN=your-callback-token
WECOM_CALLBACK_ENCODING_AES_KEY=your-43-char-aes-key
# Optional
WECOM_CALLBACK_HOST=0.0.0.0
WECOM_CALLBACK_PORT=8645
WECOM_CALLBACK_ALLOWED_USERS=user1,user2
hermes gateway
(Use hermes gateway start only after hermes gateway install has registered the systemd/launchd service.)
The callback adapter starts an HTTP server on the configured port. WeCom will verify the callback URL via a GET request, then begin sending messages via POST.
Set these in config.yaml under platforms.wecom_callback.extra, or use environment variables:
| Setting | Default | Description |
|---|---|---|
corp_id | — | WeCom enterprise Corp ID (required) |
corp_secret | — | Corp secret for the self-built app (required) |
agent_id | — | Agent ID of the self-built app (required) |
token | — | Callback verification token (required) |
encoding_aes_key | — | 43-character AES key for callback encryption (required) |
host | 0.0.0.0 | Bind address for the HTTP callback server |
port | 8645 | Port for the HTTP callback server |
path | /wecom/callback | URL path for the callback endpoint |
For enterprises running multiple self-built apps (e.g., across different departments or subsidiaries), configure the apps list in config.yaml:
platforms:
wecom_callback:
enabled: true
extra:
host: "0.0.0.0"
port: 8645
apps:
- name: "dept-a"
corp_id: "ww_corp_a"
corp_secret: "secret-a"
agent_id: "1000002"
token: "token-a"
encoding_aes_key: "key-a-43-chars..."
- name: "dept-b"
corp_id: "ww_corp_b"
corp_secret: "secret-b"
agent_id: "1000003"
token: "token-b"
encoding_aes_key: "key-b-43-chars..."
Users are scoped by corp_id:user_id to prevent cross-corp collisions. When a user sends a message, the adapter records which app (corp) they belong to and routes replies through the correct app's access token.
Restrict which users can interact with the app:
# Allowlist specific users
WECOM_CALLBACK_ALLOWED_USERS=zhangsan,lisi,wangwu
# Or allow all users
WECOM_CALLBACK_ALLOW_ALL_USERS=true
The adapter exposes:
| Method | Path | Purpose |
|---|---|---|
| GET | /wecom/callback | URL verification handshake (WeCom sends this during setup) |
| POST | /wecom/callback | Encrypted message callback (WeCom sends user messages here) |
| GET | /health | Health check — returns {"status": "ok"} |
All callback payloads are encrypted with AES-CBC using the EncodingAESKey. The adapter handles:
The crypto implementation is compatible with Tencent's official WXBizMsgCrypt SDK.
Signature verification failing.
WeCom signs every request with the Token you registered in the admin
console. A mismatch between the token configured in Hermes and the token the
admin console expects is the most common cause. Re-copy both the Token and
EncodingAESKey from the admin console — they're easy to truncate. Whitespace
in ~/.hermes/.env values around = will also break signature checks. After
fixing, restart hermes gateway run.
Callback URL not reachable / verification step fails. WeCom hits the public URL you registered. Confirm:
/wecom/callback to the gateway's port.curl -i https://<your-domain>/wecom/callback
returns something other than a timeout (a 4xx without query params is fine —
it just means the listener is reachable).Port not reachable / listener not bound.
Check hermes gateway run logs for the bound host/port. If the adapter bound to
127.0.0.1 you must front it with a reverse proxy or tunnel — WeCom's servers
can't reach loopback. Set extra.host: 0.0.0.0 in config.yaml (plus
allowed_source_cidrs if exposing directly) or keep loopback and use a tunnel
such as Cloudflare Tunnel / nginx.