☰
企业微信外部群消息推送:Webhook与客户群群发API实战解析
2026/10/20 20:00:28 网站建设 项目流程

上个月接了个需求:客户下单后,要往对应的客户服务群里自动推一条进度通知。当时我第一反应是去找企业微信“发送应用消息”接口,心想这不就是一条消息的事。结果翻了半天文档,被一句话堵了回来:应用消息只能发给企业内部成员,根本触达不了外部联系人。

这里的“外部群”指的是包含微信用户、企业外合作方成员的群。这类群里的人不在你的企业通讯录里,不能靠 userid、部门 id 寻址。后来我实际跑下来发现,真正适合做外部群主动推送的,主要就是两条路:群机器人 Webhook,以及客户群群发 API。这篇就把这两种方案的差异、调用方法和踩过的坑一次说清楚。

1. 先搞清楚“外部群主动发消息”到底卡在哪

很多第一次做企业微信开发的人,和我一开始的直觉一样:既然内部群能通过应用消息推,那外部群也应该有个类似的接口,直接指定群 ID 发一条消息就行。

实际上没有这么简单。

企业微信的安全设计里,外部联系人属于“半开放”对象。你可以读取客户群的基本信息、群成员列表,也可以创建群发任务,但不能像内部消息那样随心所欲地直接推给外部客户。原因也不难理解:如果任何开发者都能拿到群 ID 就向群里塞消息,外部群马上会变成广告重灾区,微信生态对“骚扰触达”的容忍度非常低。

所以“外部群主动发消息”这个需求,要拆成两类:

  • 实时推送型:比如服务器告警、订单状态变更。目标是一秒钟内让群里的人看到,越及时越好。
  • 任务群发型:比如运营活动、每日汇总、节日问候。目标是把消息发出去,但不需要跟着业务事件实时触发,可以走“任务审批+成员确认”的模式。

不同的类型,对应的实现方式完全不同。实时推送我最后用的是群机器人 Webhook,运营群发则用客户群群发 API。

需求类型推荐方案是否即时送达是否需要人工确认适合场景
实时通知群机器人 Webhook是否告警、订单提醒、自动化通知
运营群发客户群群发 API否,生成群发任务是,群主/成员确认发送营销活动、常规触达
一对一触达客户联系群发 API否是销售回访、个性化内容

这套认知理顺之后,再去看具体接口,就不会绕路。

2. 最直接方案:群机器人 Webhook,一条 HTTP 请求推消息进群

如果你只是想把消息主动推进某个外部群,最省事的接口其实是群机器人。

2.1 怎么拿到群机器人的 Webhook 地址

进入目标群的群设置,找到“群机器人”,添加一个自定义机器人。创建完成后,企业微信会给你一个 Webhook 地址,格式大致是:

https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=这里是一串密钥

这个地址本身就是一个 API 入口。只要向它发 POST 请求,消息就会以机器人身份出现在群里,不需要企业微信后台配置复杂的权限,也不需要获取 access_token。

我用的第一个生产环境案例,就是把这个 Webhook 接入了内部的订单系统。客户一下单,服务端直接拼接消息体发过去,群里的客户和售后同事都能第一时间看到进度。整个过程没有经过任何人的手动确认。

2.2 发一条文本消息试试

先看最基础的文本消息,用 curl 就能验证:

curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的Key' \ -H 'Content-Type: application/json' \ -d '{ "msgtype": "text", "text": { "content": "您好,您的订单已进入打包发货阶段。" } }'

正常返回是:

{ "errcode": 0, "errmsg": "ok" }

只要 errcode 是 0,消息就发出去了。

如果是在 Python 服务里调用,我习惯把它封装成一个通用函数:

import requests def send_qw_group_message(webhook_url: str, content: str) -> bool: payload = { "msgtype": "text", "text": { "content": content } } resp = requests.post(webhook_url, json=payload, timeout=10) result = resp.json() if result.get("errcode") != 0: print(f"发送失败: {result}") return False return True

注意几点:

  • 请求头要设置Content-Type: application/json,但实际传参时用json=payload即可,requests 库会自动处理编码。
  • 连接超时不要设太长。我踩过坑:某个 Webhook 地址因为网络原因一直卡住,导致业务接口响应变慢,最后统一加了 timeout 参数才解决。
  • Webhook 地址一定要走服务端保存,不能下发到前端页面里让别人拿到,否则任何人都可以往你的群里发垃圾消息。

2.3 常用的消息类型,不只是纯文本

机器人 Webhook 支持多种消息类型,实际用得最多的是 text、markdown、image、news、file 这几种。

markdown 类型的消息很适合做格式化通知:

payload = { "msgtype": "markdown", "markdown": { "content": "## 订单异常提醒\n" "> 订单号:<font color=\"warning\">SO20240501001</font>\n" "> 状态:<font color=\"comment\">发货失败</font>\n" "> 处理人:仓库 A 组" } }

news 类型则适合带跳转链接的消息:

payload = { "msgtype": "news", "news": { "articles": [ { "title": "6月活动预告", "description": "本周五会员日,全场折扣提前看", "url": "https://example.com/activity", "picurl": "https://example.com/promo.png" } ] } }

在实际项目中,markdown 是使用频率最高的。告警类消息用不同颜色区分严重级别,运营通知用标题加列表,可读性比纯文本高一大截。但要注意,markdown 消息的展示效果在不同客户端上存在细节差异,不能用过于复杂的语法,简单标题加引用加颜色就足够了。

2.4 机器人的频率限制与常见坑

机器人看着简单,但它是有隐性的“脾气”的。

最大的限制是频率。单个机器人短时间内发送频率过高,会被限流甚至直接拒绝。企业微信目前的策略大致是每分钟限制多少条、每天限制多少条,具体的数值会随产品策略调整,所以我不建议把数值写死在代码里指望永远不变,而是要做两件事:

  • 在代码里捕获返回的错误码,遇到频率限制时转入退避重试或告警降级。
  • 重要消息和备份通道做好切换。比如说,如果机器人 Webhook 被限流了,至少能退化为发邮件或者发到内部群。

另外一个很容易忽略的问题:机器人消息没有重发机制。你调接口返回成功了,消息就到群里了;如果返回失败,你重发一次是新的消息,不是对同一消息的补偿。对于“必须不重不漏”的业务场景,需要自己维护消息幂等与业务状态,不能依赖企业微信的推送结果。

还有,一旦有人把机器人从群里移除,Webhook 立即失效。企业微信不会提前通知你。所以我在服务里做了个定时健康检查,每天凌晨用一条测试消息探测所有关键群机器人是否还活着,失效的自动告警出来。

3. 带任务管理的方案:客户群群发 API,适合运营级群发

Webhook 虽然直接,但它有一个先天限制:它只负责“投递”,不关心“谁发的”。在更正式的运营场景里,你需要知道这条消息是哪位成员发出去的、客户有没有拉黑、任务有没有超时,这就轮到客户群群发 API 出场了。

3.1 获取企业微信的 access_token

群发接口走的是企业微信服务端 API,需要先通过 corpid 和 secret 换取 access_token:

import requests CORP_ID = "你的企业ID" SECRET = "客户联系应用的Secret" def get_access_token(corp_id: str, secret: str) -> str: url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" params = { "corpid": corp_id, "corpsecret": secret } resp = requests.get(url, params=params, timeout=10).json() if resp.get("errcode") != 0: raise RuntimeError(f"获取access_token失败: {resp}") return resp["access_token"]

这里的 secret 是“客户联系”应用的 secret,不是随便一个自建应用的 secret。如果你的企业微信没有开通客户联系功能,群发相关的接口会提示无权限。这一点我在开始的时候踩了不少坑,先确认权限,再调代码。

3.2 获取客户群列表

要群发,首先得拿到客户群的 ID。客户群列表接口可以按条件筛选群聊:

def get_customer_group_list(access_token: str, cursor: str = ""): url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_customer_group_list" params = {"access_token": access_token} body = { "limit": 100, "cursor": cursor } resp = requests.post(url, params=params, json=body, timeout=10).json() return resp

返回结果里会有一组 group_chat_list,每个群包含 chat_id。拿到 chat_id 之后,你才能创建群发任务。

如果只是想向指定群发,也可以跳过“列表”这一步,直接把已知的 chat_id 填到群发接口里。但查列表能帮你确认这个群是否还存在、群名是否对得上,避免拿到一个已经解散的群 ID。

3.3 创建群发任务:add_msg_template 接口

核心接口是:

POST /cgi-bin/externalcontact/add_msg_template

请求体大致是这样:

body = { "chat_type": "group", "chat_id_list": ["wrOgQhDgAAcwMTBZ3P8mQ1IuHkYyJ01w"], "text": { "content": "本周六会员日,全场商品享 8 折,点击小程序查看详情。" }, "attachment": { "msgtype": "link", "link": { "title": "会员日专场", "picurl": "https://example.com/pic.png", "desc": "精选好物,限时折扣", "url": "https://example.com/sale" } } }

调用之后会返回一个 msgid,这个 msgid 就是群发任务的标识:

{ "errcode": 0, "errmsg": "ok", "msgid": "msgxxxxx" }

但这里有一个最关键、也是最容易被新手误解的点:add_msg_template 不会把消息立刻送到群里。

它只是创建一个群发任务。任务生成之后,对应客户群的群主或管理员会在企业微信 App 里收到一条待发送提醒,需要他们在客户端手动点击确认,消息才会真正发出去。如果群主不点,任务就一直挂着。

在企业微信产品设计里,这个机制是为了防止企业随意粗暴地触达客户,把“群发”这个动作的最后一步交给真人,而不是让后台脚本无节制地轰炸。所以如果你追求的是“服务器一调,消息立刻到群里”的体验,这个接口并不合适。

3.4 群发之后的查询与跟进

群发任务创建后,可以用发送结果接口查询执行情况:

GET /cgi-bin/externalcontact/get_group_send_result?access_token=ACCESS_TOKEN&msgid=MSGID

返回结果里能看到每个群的状态,比如是否已发送、是否被客户拒收等。我建议做运营群发时,不要只发不管,而是在创建任务后隔一段时间轮询一次结果,把“未发送”的群单独捞出来,安排人工跟进。

群发 API 还有一条很重要的限制:同一个客户每天最多只能收到一条来自企业的群发消息。也就是说,如果你今天已经给一批客户群里发过消息了,再发第二条,接口会直接报错。这个限制是为了保护用户体验,做高频率运营的人必须做好排期,别等到当天去撞限流。

3.5 Webhook 和群发 API 怎么选

这两者不是替代关系,而是互补关系。

对比维度群机器人 Webhook客户群群发 API
即时性即时推送需要成员确认,非即时
接入成本极低,一个URL即可需要开通客户联系、换 access_token
消息类型text、markdown、image、news、file文本、图片、链接、小程序等
是否计入防骚扰限制按机器人频率限制按客户每日群发上限限制
运营数据无,发完即结束可查询发送状态
典型场景告警、实时通知营销活动、定期触达

实时事务类消息,我全部走 Webhook;有运营节奏、需要留痕和追踪的批量消息,走群发 API。结合起来用,基本能覆盖外部群主动推送的全部主要场景。

4. 调用细节与错误码排查:为什么你的消息就是发不出去

这类接口报错,大多不是代码逻辑问题,而是权限、token、参数格式这三类。

4.1 access_token 相关的常见错误

出现频率最高的是这几类:

错误码错误含义常见原因
40001不合法的 secretcorpid 和 secret 不匹配,或 secret 配错了应用
42001access_token 已过期token 没有走缓存,重复获取后过期
40014不合法的 access_tokentoken 拼写错误,或已经过期且未刷新
48002接口无权限未开通客户联系功能,或 secret 不是“客户联系”应用的
60020访问来源 IP 不合法服务端出口 IP 不在企业微信后台的可信 IP 白名单里

最后一个我印象很深。第一次在测试环境联调群发接口时,明明本地跑得好好的,部署到服务器上就开始报错。查了半天才发现,企业微信后台对调用接口的服务器 IP 有白名单限制。你需要登录企业微信管理后台,把服务器的公网出口 IP 加到“客户联系”或“应用管理”的对应信任列表里。

4.2 接口返回成功,群里却收不到消息

这一类问题在 Webhook 和群发 API 里都可能出现。

Webhook 机器人返回失败,多半是频率限制。返回成功但没消息,先检查机器人是不是已经被移出群了。我之前遇到过部署环境里误用了旧 Webhook 地址的情况,数据是发到老的测试群去了,调试群里当然看不到消息。

群发 API 返回成功但群里没消息,十有八九是卡在了“成员确认”这一步。任务已经生成,但群主没有在客户端点发送,或者群主属于不活跃成员,一直没看到待办。

另一种情况是接口返回了 group_chat_id 不存在或已失效。外部客户群如果已经解散,或者企业成员主动退出了群聊,群 ID 也会失效。所以调接口之前,先查一遍客户群列表,确认这个 chat_id 还在,再发起群发。

4.3 联调环境的搭建建议

企业微信开发有一个很方便的机制:每个企业可以有多个自建应用,但生产环境的应用和测试环境的应用最好完全分开。我用的是“测试企业”模式,申请一个测试企业微信账号,在里面创建客户联系应用,添加几个模拟客户,所有接口联调都在测试环境完成。

这样有几个好处:

  • 不会误发消息给真实客户。
  • 可以反复调整消息内容和频率限制,不用担心被投诉。
  • 错误码返回后再 debug,不会污染生产数据。

等测试环境全部验证完,再替换成生产环境的 corpid 和 secret,代码一行不用改,直接上线。这个习惯帮我躲过了不少线上事故。

5. 真实的落地姿势:从告警机器人到定时群发任务

最后分享一套我目前在生产环境实际使用的组合方案,供参考。

5.1 写一个通用的 Webhook 发送模块

不管是告警还是定时通知,我都会先写一个具备基础能力的发送模块:

import hashlib import time import requests class QwGroupNotifier: def __init__(self, webhook_url: str): self.webhook_url = webhook_url def send_text(self, content: str, dedup_key: str = ""): if dedup_key: # 简单的幂等去重,避免重复推送 digest = hashlib.md5(dedup_key.encode()).hexdigest() if self._seen(digest): return True payload = { "msgtype": "text", "text": {"content": content} } resp = requests.post(self.webhook_url, json=payload, timeout=10) return resp.json()

幂等去重很重要。告警系统经常会出现同一问题在短时间内反复触发的情况,如果不做去重,群消息会被刷屏,客户和同事都会崩溃。

5.2 组合一套定时群发任务

运营侧的每日通知,我会用定时任务配合群发 API 来跑。这里的流程是:

  1. 每天固定时间扫描需要发送的客户群列表。
  2. 调 add_msg_template 创建群发任务。
  3. 等待一段时间后查询 get_group_send_result。
  4. 把未发送的群单独汇总成表,发给运营同事去跟进。

这样做的好处是,虽然群发需要人工确认,但你至少把“创建任务”这一步自动化了,运营只需要负责“点确认”,省去重复编辑内容的时间。

5.3 个人在实际操作中的几条体会

第一,外部群消息的内容一定要克制。群发 API 有官方的每日上限,但就算没有限制,也不意味着可以无节操地刷屏。外部群的成员本质上还是微信客户,长期高频触达会带来退群和拉黑。我在实践中控制运营消息的频率,通常一周不超过两条,效果比天天发要好得多。

第二,Webhook 地址和 secret 一样属于敏感信息。别把它放在前端代码里,也不要写进公开仓库。之前有人把测试环境的机器人地址提交到 GitHub 上,导致机器人被脚本刷了一整天垃圾消息,最后只能删掉机器人重新创建。

第三,错误处理要做得冗余。企业微信接口也会偶发超时,不能因为一次失败就放弃整个任务。我的策略是:文本通知类消息重试两次,采用指数退避;群发任务则记录失败状态,等待下次轮询补发。

第四,不要把“发出去”等同于“客户看到了”。Webhook 返回 ok,只能代表消息送到了企业微信服务器,不代表每个群成员都打开了群。运营数据要想准确,还是要依赖企业微信侧的数据报表,不能只凭发送日志做判断。

外部群主动发消息,本质上是一个围绕“触达边界”的设计题。Webhook 负责快,群发 API 负责稳,把两者用对场景,你就能在不骚扰客户的前提下,把该推的消息推到位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询