NEW

免费试用已开放

立即开始

自动化 · 回调 · Webhook

企业微信 Webhook 如何配置

更新于 2026-06-0910 分钟

Webhook 配置看似只是「填个地址」,但要稳定收事件,地址可达性、验签、响应时延与重试处理缺一不可。下面给出一份可照做的清单。

「配置 Webhook」实际要做的是四件事

控制台里那张表单只有一个地址栏和一组订阅项,填完保存看起来就结束了。但决定这条链路能不能用的,是表单之外的四件事:入口在公网上是否稳定可达、每一个请求的来源怎么校验、返回 2xx 到底承诺了什么、同一个事件被送来第二次时会不会产生第二次副作用。任何一件没做,表现都是「配置好了但不好用」。

  • 可达性:一个走 HTTPS 的地址,证书链完整,而且这条路径没有被你自己的鉴权中间件挡下。
  • 身份:常态验签回答的是「这一次推送是不是从平台来的」,它和保存配置时那一次握手验证不是同一件事。
  • 响应契约:先返 2xx 表示「收下了」,处理结果不在这次响应里表达。
  • 重复语义:投递方拿不到响应时,分不清「没送到」和「送到了但回执丢了」,所以重复投递是固有语义,不是故障。

站内《企微回调接口怎么配置和验签》把其中配置与验签两段拆得更细:地址粒度怎么定、订阅开多宽、握手和常态验签为什么必须写成两个分支。本页是这个主题的总纲,负责把整条链路从准备到验收串一遍。

配置前要先准备好的东西

下面这些不齐就开始填地址,后面每一步都会返工。

  • 一个可公网访问、走 HTTPS 的回调地址。本地开发没有公网地址,用内网穿透拿一个临时域名先顶上。
  • 一个薄接收端。它只负责收下事件,要能独立扩容和重启;挂在会频繁发布的业务进程上,每发一次版就丢一批事件。
  • 落盘能力。原始报文按天保留,验签失败的那些也留着,事后重放和排障全靠它。
  • 一把调用密钥,事件到达后回写要用。密钥在控制台 https://console.wecomapi.com 创建与轮换。
  • 一个响应时延预算。接收端要能在数百毫秒内返回,重活一律挪到异步。

有个坑几乎每个团队都会撞一次:回调路径被自己的全局鉴权挡了。回调请求既没有 Cookie 也没有你们签发的 Token,于是整整齐齐全被拒在门外,日志里只剩一片鉴权失败。这条路径要显式排除在业务鉴权之外,它的身份校验由验签负责,两套机制不要叠着用。

分步骤配置:每一步都有能判断成没成的判据

  1. 1先把接收端跑起来,再去填地址。这一版只要能接受 POST、把原始报文落盘、返回 200 就够。判据:在公网上用 curl 打这个地址拿得到 200,且落盘目录里出现了对应记录。
  2. 2在控制台填写回调地址,并勾选要订阅的事件类型。只订当前真的会处理的类型,全开的代价不是流量而是噪声,之后每次排查都要先做一遍筛选。判据:保存不报错,配置状态正常。
  3. 3处理保存时的那一次握手验证。它有自己的回应格式,写成单独一个分支并加一行注释说明什么时候会被调到;混进业务分支,下次重构很容易被顺手清掉,而失效的表现是「改配置保存不上」,跟当时改的代码看不出任何关联。判据:接收端日志里能看到这一次请求,控制台保存成功。
  4. 4补上常态验签。取原始字节算摘要、用常量时间比较函数比对、再校验时间戳窗口,四步各自打点;只打一句「签名错误」等于把排查交给运气。签名算法与参与计算的字段以线上接口文档为准。判据:真实事件能通过,而手工改掉一个字节的伪造请求被拒。
  5. 5把业务处理挪到 ACK 之后,落盘仍然放在 ACK 之前。把落盘也挪到后面看着响应更快,实际是在承诺一件没做到的事:你已经说收下了,事件却只活在内存里,进程一重启就没了。判据:接收端的响应时延和下游快慢不再相关。
  6. 6接上回写。命中规则后,由队列另一头的消费者调用发送接口,不要塞进回调请求的生命周期。判据:发一条测试消息,几秒内能看到自动回复,并且这次调用的 x-request-id 在日志里检索得到。
示意:验签 → 落盘 → 快速 ACK → 入队javascript
// 示意逻辑,签名算法与参与计算的字段以线上接口文档为准
app.post("/wecom/callback", raw({ type: "*/*", limit: "256kb" }), (req, res) => {
  // req.body 必须是原始字节:用解析后的对象重新序列化再算摘要,一定对不上
  if (!verifySignature(req.body, req.headers)) return res.sendStatus(401);

  persist(req.body);   // ACK 之前只做落盘这一件事
  res.sendStatus(200); // 快速 ACK,之后的代码都不在对方的耐心范围内了

  // 幂等、业务处理,以及回写(POST https://manager.wecomapi.com/message/sendText)
  // 都在队列的另一头做,不放进这个请求的生命周期
  enqueue(req.body);
});

架构与数据流:谁调谁、状态存在哪

一条事件走完全程会经过四个角色,职责不重叠:平台把事件推给接收端,接收端验签、落盘、ACK;队列把事件交给消费者,消费者执行业务并在需要时回写。

  • 接收端:无状态,只做验签、落盘、ACK 三件事,可以随时重启和横向扩容。
  • 存储:原始报文和幂等记录都在这里。幂等要做在业务处理入口,不能只做在 HTTP handler,handler 里的去重挡不住队列自己的重投。
  • 队列:按会话维度分区,同一个会话内保持顺序,不同会话并行消费。
  • 消费者:执行业务,失败进重试队列,反复失败进死信并告警,原始报文留着支持手工重放。

重试的责任分两层,判据是「这次失败重投一遍会不会好」。会好的属于投递层,比如网络抖动、你正在滚动重启,交给平台重试合适;重投一百次结果都一样的属于业务层,比如下游库挂了、数据不合法,交给自己的重试队列和死信兜底,让平台反复重试只是在浪费双方资源。

这两层里的具体取舍,站内各有一篇细讲:《企业微信消息处理为什么必须走队列》讲同步处理的三种崩法、队列按会话还是按账号切分、以及重放和死信怎么做才安全;《企业微信 Webhook 重复投递与幂等设计》讲幂等键该拼什么、去重窗口按什么定、去重记录放 Redis 还是放数据库唯一索引。这两处定错,后面改动成本很高。

错误处理与排障

回调出问题时最常见的对话是「你们没推过来」和「我们推了」,两边各拿一份日志,谁也证伪不了谁。解法不是继续争,是把链路切段,每段定一个能一刀切开责任的判据。

  1. 1一条都收不到:先在公网上直接 curl 这个地址,把 DNS、证书链、路径三样各确认一遍。证书过期或者中间证书链不全,在浏览器里可能只是个警告,在服务端的 HTTP 客户端那边通常直接是连接失败,而且失败得非常安静,没有人会收到通知,事件就是不来了。
  2. 2能收到但全被拒:看验签四步的分步日志。本地能过、线上过不了,八成不是算法问题,而是报文在到达验签函数之前被谁改过:网关补头、日志中间件读完 body 又塞回去、压缩层解压后再交给你,任何一处都会让字节和对方签名时的那份不一样。
  3. 3收得到但很慢:看接收端响应时延的分布,不要看平均值。平均值会被大量快速返回拉平,真正触发重试的是尾部那几个百分点。
  4. 4同一件事被执行了两次:幂等没生效,或者位置放错了。先确认去重是不是只写在 HTTP handler 里,再确认幂等键取的是不是平台下发的事件唯一标识。
  5. 5事件收到了但回写失败:拿响应头里的 x-request-id 去定位那一次调用。如果同时看到 x-ratelimit-remaining 掉得很低,那是频率问题而不是参数问题,退避后重试即可。

验签不通过就拒绝,别返 200 假装收下,那会把一次密钥配错变成一场静默的数据丢失,而且没有任何指标会亮。反过来,能验签但解析不出来的报文要收下、落盘、再告警:这通常意味着对端发了新版本或者你的解析有问题,丢掉就再也查不回来。

《企业微信事件回调延迟与丢失怎么排查》把上面这套分段做成了网络层、验签、响应时延、队列积压、业务异常五段定位法,每段给了该看哪条日志、哪个数能定责;本页只列到「能分到段」为止。

怎么验证配置真的做对了

保存成功不等于链路可用。下面五条都在预发环境跑一遍,每条都有一个可观察的结果,不靠「看起来没问题」下结论。

  1. 1造一次真实事件,比如给托管账号发一条消息,确认接收端日志里出现对应记录。握手成功只说明这个地址是你的,不说明事件推得过来。
  2. 2手工改掉签名里的一个字节再发一次,确认返回的是拒绝而不是 200。
  3. 3把同一份原始报文重放两次,确认业务只执行了一次:库里只有一条记录,也只发出一条回复。
  4. 4故意让下游超时一次,确认接收端仍然在几十毫秒内返回 2xx,事件堆在队列里而不是消失。
  5. 5直接用命令行调一次发送接口,确认响应体是 code 为 0、msg 为 success,并且响应头里的 x-request-id 能在自己的日志中检索到。
示意:联调地址与回写通路各验一次bash
# 1. 本地没有公网地址时,用内网穿透拿一个临时 https 域名填进控制台
ngrok http 3000
# => https://xxxx.ngrok.app  ->  http://localhost:3000

# 2. 验回写通路。-i 把响应头一起打出来,可追溯的标识就在响应头里
curl -i -X POST https://manager.wecomapi.com/message/sendText \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "guid":    "7db8...",
    "toId":    "78813...",
    "content": "回调联调测试"
  }'

# 预期响应体: { "code": 0, "msg": "success" }
# 响应头 x-request-id 记进日志,排障时提供它即可定位这一次调用
# 响应头 x-ratelimit-remaining 用来判断是不是撞了频率,而不是参数写错

前四条只能在预发做,线上没有第二次机会。换地址、换域名、换证书也按有流量的变更来做:新入口先并行接一段时间,确认真的收得到事件,再摘掉旧的。具体字段、算法与限制以线上接口文档为准,示意代码不要照抄上生产。

常见问题

企业微信 Webhook 地址填什么?必须是 HTTPS 吗?
填一个可公网访问、走 HTTPS 的地址,指向你自己的接收端而不是业务主服务。证书链要完整:中间证书缺失在浏览器里可能只是警告,在服务端 HTTP 客户端那边通常直接是连接失败。另外一个环境一个地址,测试、预发、生产各配各的,用查询参数区分环境等于把一次手误兑现成给真实客户发消息。
本地开发收不到回调怎么办?
本地地址不可公网访问,需要用内网穿透(例如 ngrok)拿到一个 https 地址再填进控制台。如果穿透地址也收不到,按顺序排三件事:这条路径是不是被全局登录校验挡了、握手验证有没有正确回应、返回码是不是 2xx。穿透域名重启会变,变了要回控制台改,这段窗口内的事件收不到。
Webhook 和消息回调是一回事吗?
可以当成同一类机制:平台以 HTTP 主动把事件推到你配置的地址。区别只在讨论的层面 —— 配置层关注地址、订阅范围和握手,处理层关注验签、快速 ACK 和幂等。两层都做完,这条链路才算通。
为什么会重复收到同一个事件?
响应超时或者返回非 2xx 都会触发重投,而投递方拿不到响应时无法区分「没送到」和「送到了但回执丢了」,所以只能重发。再加上你自己队列的 at-least-once 语义,重复是必然的。做法是先快速 2xx、再异步处理,并用平台下发的事件唯一标识在业务处理入口去重 —— 只写在 HTTP handler 里的去重挡不住队列的重投。
回调返回 200 了,但业务没执行,怎么排查?
200 只承诺「收下了」,不承诺处理成功,所以要往 ACK 之后看:先确认原始报文有没有落盘,再确认有没有真的入队,最后看消费者是不是抛异常进了重试或死信。如果卡在回写这一步,拿响应头里的 x-request-id 去定位那一次调用;x-ratelimit-remaining 很低说明是频率问题,退避重试即可。
业务处理失败了,该返回非 2xx 让平台重试吗?
不该。投递层重试解决的是「没送到」,业务失败重投多少次结果都一样。照常返 2xx,把失败交给自己的重试队列和死信队列;只有接收端自身过载时才适合返非 2xx,让对方的退避机制帮你分担压力。

准备好动手了?

精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。

相关指南