Before writing any code for WeCom (WeChat Work), pick the right lane — the three approaches differ sharply in data boundary, effort, and what they can actually do
| Dimension | A: Custom App + Callback | B: Group Robot Webhook | C: Third-Party SaaS |
|---|---|---|---|
| Receive messages | ✅ Full in-app 1:1 send/receive | ❌ Push to group only | ✅ Platform relays both ways |
| Data boundary | Messages stay on your servers | Push content via WeCom servers | Conversations via third-party platform |
| Time to launch | 1–3 days (mostly callback debugging) | ~10 minutes | Half a day to 1 day (config-driven) |
| Dev effort | Medium: callback crypto + token mgmt + AI wiring | Minimal: one HTTP request | Low: web config + a few APIs |
| Best for | Production AI assistant, FAQ, workflow queries | Notifications, alerts, daily digests | Fast validation, no dev team |
One line: conversations need A, notifications fit B, quick validation uses C. The classic mistakes: trying to build a chatbot on B (it can't receive messages), or jumping to C without checking the data boundary.
This is the officially supported full path: an employee messages your custom app → WeCom POSTs the encrypted message to your callback URL → your service calls your AI → the reply goes back through the app message API. Everything stays on your servers; permissions, logs, and audit are under your control.
In the admin console → "Applications" → "Custom" → "Create". For the visible range, start with one small department as a canary. Write down three values:
corpid: "My Company" page → Corp IDAgentId: on the app detail pageSecret: app detail page (shown once — store it in a secrets manager, never in git)App detail → "Receive Messages" → "API endpoint". Fill in: URL (your HTTPS endpoint), Token, and EncodingAESKey (can be auto-generated). On save, WeCom sends a GET verification request to your URL: you must verify the signature and return the decrypted echostr as-is.
# callback_verify.py — callback URL verification (GET)
# Signature: sha1 of the sorted concatenation of token, timestamp, nonce, ciphertext
import hashlib
def check_signature(token, timestamp, nonce, encrypt_msg):
items = sorted([token, timestamp, nonce, encrypt_msg])
return hashlib.sha1("".join(items).encode()).hexdigest()
@app.route("/wecom/callback", methods=["GET"])
def verify():
encrypt = request.args["echostr"]
expect = request.args["msg_signature"]
actual = check_signature(TOKEN, request.args["timestamp"],
request.args["nonce"], encrypt)
if actual != expect:
return "bad signature", 403
# AES-decrypt with the official crypto library, return plaintext as-is
return decrypt_echostr(encrypt, AES_KEY)
The three usual causes of verification failure: wrong Token, server clock drift (run NTP), or implementing POST only and forgetting the GET verification. During debugging, log the four strings that go into the signature — the mismatch becomes obvious.
When an employee messages the app, WeCom POSTs an encrypted XML to your callback URL. Two hard constraints: you must respond within 5 seconds (otherwise WeCom retries, up to three times), and retried messages are identical (no dedup means duplicated replies). The standard pattern: dedupe and return immediately, process asynchronously, then push the reply through the send-message API.
# wecom_bot.py — receive: MsgId dedup + async processing
import threading
replying = set() # use Redis in production for multi-instance safety
@app.route("/wecom/callback", methods=["POST"])
def on_message():
xml = decrypt_msg(request.data, request.args) # official crypto library
msg_id = xml.get("MsgId")
if msg_id in replying:
return "success" # retried duplicate — swallow it
replying.add(msg_id)
threading.Thread(target=handle, args=(xml["FromUserName"],
xml["Content"], msg_id)).start()
return "success" # respond immediately, don't wait for the AI
def handle(user, question, msg_id):
try:
answer = ask_ai(question) # your own AI / knowledge-base service
send_text(user, answer) # push via the app message API
except Exception:
send_text(user, "Sorry, that request timed out. Please try again.")
finally:
replying.discard(msg_id)
The access_token is exchanged with corpid + Secret, valid for 7200 seconds, with a refresh rate limit — fetching it fresh at every call site is an anti-pattern. Cache centrally, refresh from one place. Also: the outbound IP of your server must be added to the app's trusted IP list, or send calls fail with error 60020.
# wecom_client.py — centralized token cache + send text message
import time, requests
_token, _ts = "", 0
def get_token():
global _token, _ts
if _token and time.time() - _ts < 7000: # 200s safety margin
return _token
r = requests.get("https://qyapi.weixin.qq.com/cgi-bin/gettoken",
params={"corpid": CORP_ID, "corpsecret": SECRET},
timeout=5).json()
_token, _ts = r["access_token"], time.time()
return _token
def send_text(user, content):
requests.post(
"https://qyapi.weixin.qq.com/cgi-bin/message/send",
params={"access_token": get_token()},
json={"touser": user, "msgtype": "text", "agentid": AGENT_ID,
"text": {"content": content[:2000]}}, # truncate/split long text
timeout=5)
That's Approach A working end to end. Swap ask_ai() for your RAG knowledge base or LLM service and you have a usable enterprise assistant.
Group settings → "Group Robot" → "Add" gives you a webhook URL. POST JSON to it and the message lands in the group:
curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"Deploy done: order service v2.3 is live"}}'
Its role is one-way notification: CI/CD results, monitoring alerts, daily digests. Two hard limits: it cannot receive group messages (no conversations), and each robot has a send rate limit (aggregate high-frequency alerts). Text, markdown, and rich-card types are supported. Good for "tell humans what the system did" — wrong for "humans ask, system answers".
Managed platforms let you authorize WeCom and configure a knowledge base from a web console — no callback code to write, live within half a day. Two situations fit: no dev capacity, or you want to validate the value of an AI assistant before committing engineering time.
But ask four questions before signing, or you'll pay for it later:
Symptom: the endpoint config never saves, or pushes never arrive.
Cause: Token mismatch, server clock drift, or POST implemented without the GET verification.
Symptom: occasional token fetch failures, or a new token instantly killing the old one.
Cause: multiple code paths refreshing the token and evicting each other's cache; no lock on the central cache.
Symptom: when the AI is slow, the employee gets duplicate answers to the same question.
Cause: synchronous AI calls exceed 5 seconds, WeCom retries, and the server doesn't dedupe by MsgId.
Symptom: works locally, fails with 60020 after deploying to the server.
Cause: the outbound IP is not on the app's trusted IP list.
Symptom: longer AI answers never arrive, or only the first half shows up.
Cause: text messages have a length cap; over-limit content errors out or gets cut.
No approach is universally better — the choice comes down to three things: how strict your data boundary is, how much dev capacity you have, and what scenario you're solving. Don't spend engineering on Approach A for a notification feed; don't build a chatbot on Approach B (it can't receive); don't sign up for Approach C without asking the four data-boundary questions. Many teams' real path: validate quickly with C or B, then migrate to A as the long-term self-hosted home.
If you only keep one line: conversations → A, notifications → B, validation → C — don't mix them.