← Home Blog 中文版

WeCom Bot + Self-hosted AI Service in 30 Minutes

From creating a self-built app to receiving your first AI reply inside WeCom (with 4 error fixes)

2026-08-15 · Mule Agent · Tutorial · 15 min read
TL;DR — A 30-minute reproduction checklist: WeCom admin console → create a self-built app → configure the message callback (AES decryption) → wire up your AI service → receive the first AI reply in WeCom. Four common errors and fixes at the end, each from real troubleshooting.
⚠️ Avoid the biggest trap first: the "group bot" webhook in WeCom can only send messages outward — it cannot receive messages from employees. To build a conversational AI assistant, you must use a self-built app + message callback. This guide follows that path end to end.

Step 1: Create a self-built app in the WeCom console

📌 Create app + get CorpID / AgentId / Secret 5 min
  1. Log in to work.weixin.qq.com (as company admin) → App Management → Self-built → Create App
  2. Fill in the app name/icon → open the app details page and record three values:
    • CorpID (My Company → Company Info → bottom of page)
    • AgentId (on the app details page, numeric)
    • Secret (App details → Secret → View; once you leave the page you must reset it to see it again)
  3. App details → Trusted IP: add your server's public egress IP (skip this and API calls fail with 60020 — see error 3 below)
  4. App details → Receive Messages → API receive settings: fill in a Token (any random string you make up) and EncodingAESKey (click "generate random")
  5. The "Receive Messages" page will ask for a callback URL — leave it empty for now, come back after step 3 gives you a public URL via ngrok

Step 2: Write a minimal service that passes callback verification

🐍 Python Flask callback + AES decryption 8 min

WeCom callback verification is one layer deeper than Feishu: the GET request carries msg_signature, and echostr is AES-encrypted — you must decrypt it first, then return it as-is. Copy this code:

import hashlib, base64, struct
from flask import Flask, request, Response
from Crypto.Cipher import AES

app = Flask(__name__)

TOKEN = "your-token"
AES_KEY = "your-encoding-aes-key"  # 43 chars; append "=" in code
CORP_ID = "your-corp-id"

def decrypt_msg(encrypted):
    key = base64.b64decode(AES_KEY + "=")
    cipher = AES.new(key, AES.MODE_CBC, key[:16])
    plain = cipher.decrypt(base64.b64decode(encrypted))
    pad = plain[-1]
    plain = plain[:-pad]                      # strip PKCS7 padding
    plain = plain[16:]                        # strip 16-byte random prefix
    msg_len = struct.unpack("!i", plain[:4])[0]
    return plain[4:4 + msg_len].decode()      # the message body

def verify_signature(signature, timestamp, nonce, echostr):
    s = "".join(sorted([TOKEN, timestamp, nonce, echostr]))
    return hashlib.sha1(s.encode()).hexdigest() == signature

@app.route("/wecom/callback", methods=["GET"])
def verify_url():
    args = request.args
    if verify_signature(args["msg_signature"], args["timestamp"],
                        args["nonce"], args["echostr"]):
        return Response(decrypt_msg(args["echostr"]), mimetype="text/plain")
    return "signature error", 403

@app.route("/wecom/callback", methods=["POST"])
def receive_msg():
    # WeCom POSTs { Encrypt: "..." }; verify signature, then decrypt.
    # The decrypted payload is XML: <MsgType>text</MsgType> + Content + FromUserName...
    # Log it for now; swap in the AI call in step 4.
    return Response("success", mimetype="text/plain")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

⚠️ Dependencies: pip install flask pycryptodome. WeCom requires the callback to return plain text success (not JSON) — anything else and it keeps retrying delivery.

Step 3: Expose your local service with ngrok and fill in the callback URL

🌐 Public callback address 2 min
  1. Run: ngrok http 8080 (cloudflared or bore work too)
  2. Copy a URL like https://xxxx.ngrok-free.app
  3. Back in WeCom "Receive Messages" → enter https://xxxx.ngrok-free.app/wecom/callback → Save
  4. WeCom immediately issues a GET verification request — your verify_url() returns the decrypted echostr and the save succeeds

Step 4: Replace the XML handler with an AI call

🤖 Wire up AI + send messages 10 min

The POST callback payload is XML after decryption — parse it with xml.etree, call your AI, then reply to the employee via the "send app message" API:

import requests, xml.etree.ElementTree as ET

def get_access_token():
    url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
    r = requests.get(url, params={"corpid": CORP_ID, "corpsecret": SECRET},
                     timeout=10)
    return r.json()["access_token"]          # valid 2h; cache it

def send_to_user(user_id, content):
    token = get_access_token()
    url = "https://qyapi.weixin.qq.com/cgi-bin/message/send"
    requests.post(url, params={"access_token": token}, json={
        "touser": user_id,                    # UserID, not phone/name
        "msgtype": "text",
        "agentid": AGENT_ID,
        "text": {"content": content}
    }, timeout=10)

# inside receive_msg(), after decrypting xml_str:
root = ET.fromstring(xml_str)
user_id = root.findtext("FromUserName")      # employee UserID
content = root.findtext("Content")           # the employee's text

reply = ask_ai(content)                       # call your self-hosted AI
send_to_user(user_id, reply)

Once it works end to end, add:

Step 5: Set the visible scope and go live

🚀 Go live 5 min
  1. App details → Visible Scope: start with one department (e.g. IT) for a pilot
  2. Swap the ngrok URL for an Nginx reverse proxy on your server (proxy_pass http://127.0.0.1:8080) or deploy directly
  3. My Company → WeChat Plugin — employees can now search for your app inside WeCom
  4. Send the app a message → get an AI reply → done

4 errors you will hit (enough to fill 30 minutes)

Error 1: Callback URL save keeps failing "verification failed"

WeCom verifies the URL with a GET carrying msg_signature — not a plain challenge echo like Feishu. The two most common causes: the signature string isn't built from Token/timestamp/nonce/echostr sorted then concatenated; or echostr is returned as ciphertext without decrypting.

✅ Fix: follow the code order above — sort and concatenate → SHA1 → compare → AES decrypt (strip random prefix, 4-byte length, PKCS7 padding) → return plaintext. Any wrong step fails verification with no detailed logs, so check line by line.
Error 2: Callbacks never arrive / WeCom keeps retrying delivery

The callback handler must return plain text success (Content-Type: text/plain). Return JSON or a non-200 status and WeCom retries at 3s/10s/1min intervals, flooding your logs.

✅ Fix: return Response("success", mimetype="text/plain"). Acknowledge first, then process the AI call asynchronously — otherwise slow AI responses trigger retries.
Error 3: API calls fail with 60020 "not allow to access from your ip"

All WeCom APIs check the server's egress IP. During local debugging your IP changes constantly, so calls fail with 60020.

✅ Fix: App details → Trusted IP → add your server's fixed public IP (up to 120 entries). For local debugging, temporarily add your current egress IP, or route everything through the server.
Error 4: Message sent (API returns 0) but the employee never receives it

Two most common causes: touser is set to a name or phone number, but the API only accepts UserID (the alphanumeric ID in the address book); or the app's visible scope doesn't include this employee.

✅ Fix: use FromUserName from the callback XML as touser — it's the UserID and always works. Before sending, confirm the employee is inside the app's visible scope.

After 30 minutes you will have...

Next steps (by priority):

  1. Add a knowledge base (most important — solves ~80% of business questions)
  2. Add memory (what the user asked before)
  3. Connect other IMs (Feishu / DingTalk) — Feishu guide: post 04
🤝 Replicated it and still want a ready-made one?
📧 278946228@qq.com (usually replies within 24h)