老板丢过来一句话:“把咱们服务器的告警接到企业微信里,出问题就在群里喊一声。”这种需求我在不同公司接过五六回,看起来简单,真动手才发现,光“把文本消息推到企业微信”这一个动作,背后就藏着好几条完全不同的通道。用错了接口、配错了参数,消息要么发不出去,要么连“谁发的、发给谁”都拎不清。这篇文章就以企业微信二次开发里最基础的文本消息接口为主线,把前置概念、调用流程、接收回调、注意事项一次讲透。内容偏实战,适合第一次接触企微接口的开发者、企业内部IT和做系统集成的朋友,照着操作能少走很多弯路。
1. 动手之前,先分清三套消息通道
很多人第一次查企业微信开发文档,都会被各种接口名绕晕。其实你只要记住一句话:企业微信的“消息”不是一个笼统的概念,它至少分三套完全独立的体系,各自有各自的凭证、接口和限制。
1.1 自建应用的“应用消息”通道
这是文本消息接口最正统的走法。企业在企业微信里创建一个自建应用,就能通过官方API向员工发送消息,也能接收员工主动发给这个应用的消息。做系统通知、工单提醒、日报推送,基本都是走这条路。
这个通道的关键凭证有三个:CorpID、应用Secret、应用AgentId。CorpID是企业的唯一身份标识,在管理后台“我的企业-企业信息”里能看到,是一个以ww开头的字符串;Secret相当于这个应用的密码,在应用详情页获取;AgentId是应用在企业里的数字编号,同样是应用详情页上的一串数字。三个参数缺一不可,而且必须属于同一个应用,混用就会出现各种莫名其妙的报错。
这个通道最大的优势是能精准指定接收人:你可以按成员账号(UserID)发、按部门ID发、按标签ID发,或者干脆不指定,默认发给应用可见范围内的所有人。文本内容还支持自定义超链接、换行排版、保密模式等能力。后面章节的调用流程,全部围绕这条通道展开。
1.2 群机器人的Webhook通道
如果你只是想把消息推到某个群里,又不想建应用、不想拿access_token,那就用群机器人Webhook。在任意企业微信群里添加一个“群机器人”,就能拿到一个Webhook地址,往这个地址POST一段JSON,消息就会以机器人身份出现在群里。
它最大的价值是轻量:不用管CorpID和Secret,拿到Webhook就能发。缺点是只能发不能收(除非额外配置机器人的消息接收回调),而且频率限制比应用消息严格。适合做群告警、组内通知、报表推送这类“单向广播”场景。
1.3 客户联系/外部联系人通道
还有一种常见误会:想给外部客户发消息,结果拿着应用消息接口去调,发现touser填什么都是“成员不存在”。给外部联系人发通知属于“客户联系”体系,走的是另一套接口(比如企业群发、入群欢迎语),凭证也是单独申请的客户联系Secret。
这里先不展开,但你脑子里必须绷一根弦:内部应用消息、外部客户消息、群机器人消息,是三个独立的分支,不能混着调。
下面的表格可以帮你快速做通道选型:
| 通道 | 认证方式 | 能接收消息吗 | 典型场景 |
|---|---|---|---|
| 自建应用消息 | CorpID + Secret + AgentId | 能,通过回调 | 系统通知、工单提醒、定向推送 |
| 群机器人Webhook | Webhook地址 | 可配置回调 | 群告警、组内通知、报表推送 |
| 客户联系消息 | 客户联系Secret | 受限 | 给客户群发、入群欢迎语 |
我为什么建议新手先从自建应用消息入手?因为它最完整,既能体验发送,又能体验接收回调,后面你要做机器人大模型问答、做H5免登联动,都离不开这套基础能力。
2. 文本消息发送:从建应用到调通接口的完整链路
这一章是全篇的主干。我会按一个正常项目的推进顺序,带你从零把一条文本消息真实发出去。
2.1 先在管理后台创建一个自建应用
登录企业微信管理后台,找到“应用管理-自建”,点“创建应用”。填上应用名称、上传Logo、选择可见范围,应用就建好了。
创建完成后进入应用详情页,能看到两个关键值:AgentId和Secret。AgentId一般是个数字;Secret需要点击查看,首次查看要验证管理员身份。同时在“我的企业-企业信息”里把CorpID复制出来,三个值放一起保存好。
这里真心建议:开发阶段就把可见范围设置成一个测试部门或者测试成员,别选“全体成员”。否则调试的时候手一抖点了发送,全公司都收到一条“这是一条测试消息”,那画面太美,我经历过。
2.2 获取access_token:决定了后续所有接口的流畅度
所有调用企业微信API的请求,都要先拿到一个access_token。它相当于临时门禁卡,有效期7200秒(2小时),过期后必须重新获取。
获取方式很简单,一个GET请求:
curl "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=你的CorpID&corpsecret=你的Secret"正常返回的JSON长这样:
{ "errcode": 0, "errmsg": "ok", "access_token": "xxxxxx", "expires_in": 7200 }很多新手一上来就是每次请求前都调一次gettoken,这其实是有问题的。这个接口本身有频率限制,而且高并发下每个请求都去拿一次token,白白增加延迟。
正确做法是缓存起来:第一次获取后存到内存或Redis里,记录过期时间,在过期前几分钟主动刷新。我写了一个最简版本的TokenManager,你直接抄就行:
import time import requests class TokenManager: def __init__(self, corpid, secret): self.corpid = corpid self.secret = secret self._token = None self._expire_at = 0 def get_token(self): if self._token and time.time() < self._expire_at - 60: return self._token resp = requests.get( "https://qyapi.weixin.qq.com/cgi-bin/gettoken", params={"corpid": self.corpid, "corpsecret": self.secret}, timeout=3 ).json() if resp.get("errcode") == 0: self._token = resp["access_token"] self._expire_at = time.time() + resp["expires_in"] return self._token raise RuntimeError(f"gettoken failed: {resp}")生产环境建议把token放到Redis里,多实例部署时才不会出现“A实例刚刷新token,B实例又去刷新一次”的情况。企微对token并发刷新虽然没卡得很死,但没必要去挑战这个边界。
2.3 封装文本消息发送请求
拿到access_token之后,发送文本消息的接口是:
POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN请求体是最关键的部分:
{ "touser": "ZhangSan|LiSi", "msgtype": "text", "agentid": 1000002, "text": { "content": "你的快递已到,请携带工卡前往邮件中心领取。" }, "safe": 0 }字段逐个说明:
touser:接收人的UserID,多个用|分隔。不传touser时可以用toparty(部门ID)或totag(标签ID)代替。如果三个都不传,系统默认发给应用可见范围内所有成员,这个操作要非常谨慎。msgtype:固定填text,这就是文本消息接口里“文本”的定义。想发markdown、图片、文件,这个字段要换成对应的值。agentid:你在应用详情页看到的那个数字ID,填错会报错。text.content:消息正文,最长2048字节。safe:0是普通消息,1是保密消息。保密消息在客户端不能复制、转发、下载等,适合发合同编号、工资条之类的内容。
配套一个发送函数:
def send_text(token, agentid, touser, content): url = "https://qyapi.weixin.qq.com/cgi-bin/message/send" payload = { "touser": touser, "msgtype": "text", "agentid": agentid, "text": {"content": content}, "safe": 0 } resp = requests.post( url, params={"access_token": token}, json=payload, timeout=5 ).json() if resp.get("errcode") != 0: print("发送失败:", resp) return resp有一个极其常见的问题:返回码是0,但成员就是没收到。绝大多数原因是touser填成了姓名或手机号。企业微信接口里的UserID是通讯录里设置的“账号”,一般是个英文字符串,不是中文名。去通讯录管理后台看一眼成员的账号是什么,再回来填。
2.4 高频返回码对照表
| errcode | 含义 | 常见原因 |
|---|---|---|
| 0 | 成功 | 无 |
| 40001 | access_token无效或过期 | token缓存有问题,或secret配置错误 |
| 40014 | 不合法的access_token | token拼写错误、带上了多余空格 |
| 42001 | access_token超时 | 缓存策略未按7200秒刷新 |
| 40096 | agentid不匹配 | 请求里的agentid和secret不是同一个应用 |
| 60011 | 成员不在可见范围 | 应用可见范围没包含该成员 |
| 60020 | 访问IP不在白名单 | 应用配置了企业可信IP,来源IP不匹配 |
| 45009 | 接口调用超过频率限制 | gettoken太频繁或消息发送太密集 |
| 301002 | 成员不存在 | touser填了一个无效UserID |
开发阶段建议把返回的完整JSON打印出来看。很多问题从errmsg里能直接看出端倪,别只看errcode就完事。
3. 接收文本消息与被动回复:回调这块最容易卡人
很多需求不只是“发出去”,还要“收回来”。比如员工在企业微信里对应用说“查一下工单状态”,应用要能接到这句话,再回一句结果。这就必须配置接收消息服务器,也就是常说的“回调”。
3.1 先过URL验证这一关
在企业微信管理后台,应用详情页往下拉,找到“接收消息”设置,会让你填三个东西:URL、Token、EncodingAESKey。
URL:你自己服务器的接口地址,必须公网可达,并且要同时支持GET和POST请求。Token:自己定的随机字符串,用于签名校验,相当于一个口令。EncodingAESKey:43位字符串,用于消息加解密,可以直接用系统自动生成的。
点保存时,企业微信服务器会立刻往你的URL发一个GET请求,带上四个参数:msg_signature、timestamp、nonce、echostr。你的服务必须完成三步:
- 把
Token、timestamp、nonce三个字符串按字典序排序,拼接成一个字符串,做SHA1签名,结果要和msg_signature完全一致。 - 用
EncodingAESKey对echostr做AES解密,得到解密后的明文。 - 把解密后的明文原样返回给企业微信服务器。
只要其中任何一步不对,后台就会提示“URL验证失败”。URL验证失败是回调功能的第一大坎,我见过有人卡在这里两三天。
强烈建议:不要手写AES解密逻辑,直接用企业微信官方提供的加解密库,比如Python版的WXBizMsgCrypt,里面封装好了VerifyURL、DecryptMsg、EncryptMsg三个方法,拿来就能用,不要自己造轮子。
3.2 收到文本消息后,拿到的XML长什么样
URL验证通过后,每当有人给应用发消息,企业微信服务器就会POST一个加密的XML到你的URL。解密之后,一条文本消息的内容是这样:
<xml> <ToUserName><![CDATA[CorpID]]></ToUserName> <FromUserName><![CDATA[ZhangSan]]></FromUserName> <CreateTime>1348831860</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> <MsgId>1234567890123456</MsgId> <AgentID>1000002</AgentID> </xml>重点字段:
FromUserName:发送消息成员的UserID,你要知道“谁发的”就看它。Content:文本消息正文。MsgId:消息ID,可用于去重。企业微信回调在网络异常时会重试,同一个MsgId可能推送不止一次,业务处理前一定要先判重。AgentID:这条消息是发给哪个应用的。如果一台服务器同时接了多个应用的回调,就靠它来区分。
3.3 被动回复:5秒内必须响应
在回调接口里,处理完收到的消息后需要“应答”企业微信服务器。如果你希望直接回一段文本给用户,就构造一段被动回复消息XML,加密后放在HTTP响应体里返回:
<xml> <ToUserName><![CDATA[ZhangSan]]></ToUserName> <FromUserName><![CDATA[CorpID]]></FromUserName> <CreateTime>1348831860</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[收到,处理中。]]></Content> </xml>两个最容易写反的点:
ToUserName要填发送方的UserID,也就是你收到的FromUserName;FromUserName要填企业CorpID。这个方向在逻辑上反直觉,特别多人栽在这里。- 整个回调接口最好在5秒内返回。你的业务逻辑如果比较重,比如要查询数据库、调外部API,千万别在回调里同步等结果。先返回一个“处理中”的提示,后台再用主动发送接口把结果推过去。回调超时后,企业微信会重试甚至直接报错,用户体验会变得很差。
3.4 主动发送和被动回复,场景别搞混
被动回复适合“即时问答”场景。优点是响应路径短、不需要access_token;缺点是有5秒的响应窗口,只能回文本或图片这类基础消息,而且只能回给当前会话的用户。
主动发送(message/send)没有这些限制,可以指定任意可见范围内的成员,适合异步通知、定时任务、转人工后的结果推送。实际项目基本都是“回调接收 + 异步处理 + 主动推送”的混合模式——回调接口收消息、返回占位提示,业务逻辑放消息队列里慢慢跑,最后通过主动发送把最终结果交给用户。
4. 文本消息二开发中容易踩的坑,一次列全
这一章我按“血泪程度”排序,把开发中经常遇到、但文档里又不显眼的问题集中讲一遍。
4.1 IP白名单到底卡在哪一步
企业微信的自建应用里可以配置“企业可信IP”。配了之后,使用该应用的Secret获取到的access_token,在调用其他接口时会对来源IP做校验,IP不匹配就报60020。
所以排查顺序是:先看报错是不是60020。是的话,检查应用详情页有没有配可信IP,再确认服务器出口IP是否在白名单里。一个小坑是很多服务器走NAT出口或代理,实际出口IP和你以为的不一样,先确认一下再改白名单。
4.2 文本内容细节:换行、超链接、字节长度
文本消息里的换行符是\n,不是<br>,也不是字面量两个字符“反斜杠n”。很多人在代码里写的是字符串"\\n",结果消息里真的出现了一个字母n,就是因为转义层数搞错了。
超链接要用HTML标签包一层:
你的报告已生成:<a href="http://example.com/report">点击查看</a>直接发裸链接也能点,但用<a>标签可以自定义显示文字,格式更正式,也更不容易被当成垃圾链接。
文本长度上限是2048字节。特别提醒:一个中文字符占3字节,一个emoji占4字节,你按“字符数”数了半天以为没超,其实早就超了。稳妥做法是发送前用字节长度判断,超了截断或者改用消息卡片。
4.3 群机器人Webhook别乱用
群机器人Webhook虽然方便,但有明确限制:每个机器人每分钟最多20条消息,content最长也是2048字节。超了直接报频率限制错误。
另一个安全问题是:Webhook地址里的key参数就是身份凭证,泄露了任何人都能往你群里发消息。我见过有人把Webhook地址直接写在前端网页源码里,结果群里被刷了一晚上广告。Webhook地址一定要放在服务端,至少做个转发代理,不要暴露到公网页面或公开仓库里。
4.4 消息防抖:enable_duplicate_check参数
告警类消息最怕重复轰炸。某服务每隔5秒报一次错,你的告警系统就跟着发一次通知,一晚上几百条,群里直接炸锅。
message/send接口支持幂等控制,请求体里加上两个参数:
"enable_duplicate_check": 1, "duplicate_check_interval": 1800表示同一个应用发送的相同内容,在1800秒内不会重复送达。配合告警系统的降噪策略,能把群里的告警从“轰炸”变成“一条”,这个参数建议所有做告警接入的人默认加上。
4.5 消息撤回的窗口很短
企业微信有撤回应用消息的接口,但撤回窗口并不长,而且只对主动发送的应用消息有效。作为兜底手段可以接一个“撤回”入口,但核心还是发送前多校验。特别是不要拿生产环境的Secret瞎测试,发出去的消息想撤可没那么随意。
5. 从文本消息起步,能延伸出来的常见玩法
文本消息接口只是地基,但它能承载的玩法其实相当多。结合最近圈子里讨论比较多的几个方向,简单聊聊。
5.1 告警机器人:群机器人 + 应用消息组合
我接触过的运维类需求,基本都是双通道并行。群机器人负责把告警推到公共告警群,大家都能看到;自建应用消息负责把详细工单推给当班负责人,点对点触达。群机器人不需要token、零门槛,应用消息又能精确指定人,两者配合非常顺。
5.2 接入大模型做群内自动问答(比如DeepSeek)
群里经常有人问“企业微信接入DeepSeek怎么弄”,本质就是把文本消息接口当成输入输出管道。成员在企业微信里给应用或机器人发文本消息,回调把文本内容拿到,转给大模型API,拿到回答后再走被动回复或主动发送还回去。
这里最核心的工程量其实不在模型,而在消息收发、会话上下文管理、超时处理这三件事。而这些都建立在本文讲的文本消息接口基础上,所以说基础打牢了,上层玩法就是水到渠成的事。
5.3 Webhook推送结构化数据
运营数据、日报、库存表,用纯文本硬排版很难看。群机器人Webhook支持markdown类型,可以在content里用表格语法:
{ "msgtype": "markdown", "markdown": { "content": "## 今日销售数据\n| 渠道 | 单量 |\n| --- | --- |\n| 线上 | 120 |\n| 门店 | 80 |" } }企业微信客户端对markdown表格的渲染基本可用,但长表格记得精简列数,移动端太宽的表格会被压成一长串字符,阅读体验很差。
5.4 与H5免登结合,闭环更完整
如果你有一个自建的H5页面,想在企业微信里免登录打开,并且操作后通过应用消息通知相关人员,这套组合很常见。H5通过OAuth2静默获取成员身份,前端提交业务数据到后端;后端拿到操作用户的UserID后,用文本消息接口给下一步处理人发提醒。文本消息接口在这里就是“人找人”的送达管道,也是很多企业内部系统做移动化改造的标配姿势。
把文本消息接口跑通,企业微信二开的地基就算打好了。我在实际项目中的习惯是,先在一个临时工程里按“拿token → 发文本 → 配回调 → 收消息 → 被动回复”的顺序把链路整个走一遍,确认没毛病再往正式项目里搬。还有一句血泪提醒:测试消息永远先发给自己或测试群,等确认无误再扩大范围。企业微信消息发出去容易,想撤可没那么随意。