Webhook / Update 协议

统一说明 Update 事件结构、Webhook 验签和幂等处理。

Update 顶层结构

{
  "update_id": 10001,
  "type": "message",
  "message": {
    "message_id": "msg_xxx",
    "chat": {
      "type": "private",
      "id": "au_xxx"
    },
    "from": {
      "type": "user",
      "openid": "au_xxx"
    },
    "msgType": "text",
    "data": {
      "content": "hello"
    }
  }
}

关键字段

字段说明
update_id事件递增 ID,用于去重和 offset 推进。
type事件类型,如 messagecallback_query
chat.typeprivategroup
from.type来源对象类型,常见为 userbot
from.openidBot 作用域内的用户标识。
msgType消息类型,第一版主路径以 text 为主。

Webhook 验签

第一版签名规则采用 signed_update_v1 包装。目标是让第三方开发者不需要依赖 Fly 服务端源码,也能独立完成验签。

  • Webhook 必须验证平台签名,不能只按来源 IP 信任请求。
  • 签名失败、时间戳过期、重复请求都必须拒绝。
  • 如果需要重放验证,请保留原始请求体和关键 header。
  • Phase 0 的密钥口径是:webhook_secret = sha256(bot_token)

字段说明

字段说明
update_id全局递增 Update ID;Webhook 重试时保持不变。
typeUpdate 类型,如 messagecallback_query
bot.idBot public_id。
timestamp签名时间戳,秒级 int,用于防重放窗口。
nonce随机串,配合 timestamp 降低重放风险。
signatureHMAC-SHA256 十六进制签名。
messagetype=message 时存在。
callback_querytype=callback_query 时存在。

v1 签名字符串

update_id
update_type
bot_id
timestamp
nonce
payload_sha256
  • 签名算法:HMAC-SHA256。
  • payload_sha256 来自 message 或 callback_query 的 canonical JSON。
  • canonical JSON 需要稳定排序,避免跨语言序列化差异。
  • timestamp 默认按 300 秒窗口做防重放校验。

Webhook 重试口径

  • Webhook 是 at-least-once 投递,不是 exactly-once。
  • 失败时会重试,重试应保持同一个 update_id
  • 接入方必须按 update_idcallback_query_id 做幂等去重。
  • 响应体过大也会视为失败;当前验收口径里 10KB 以上会命中 RESPONSE_TOO_LARGE

最常见的验签失败

错误含义优先排查
SIGNATURE_MISMATCH签名不一致。canonical JSON、payload_sha256、secret 是否正确。
REPLAY_WINDOW_EXCEEDED时间戳超出重放窗口。服务端时间同步、请求延迟、回放样本是否过期。
INVALID_SIGNATURE_FORMAT签名不是 64 位 hex。是否截断、是否误把 base64 当 hex。
RESPONSE_TOO_LARGE接收方返回体过大。Webhook handler 是否输出了大对象或调试日志。

幂等与去重

  • 对 Update 消费侧,以 update_id 做去重。
  • 对发送侧,以 client_msg_id 做幂等控制。
  • 不要假设 Webhook 或 Polling 只会收到一次同一事件。

群聊默认规则

  • 群聊默认是 mention_only,只有 @Bot 或指定命令才会投递。
  • 群里多 Bot 场景,不应默认广播给所有 Bot。
  • 群监听和更高风险能力是否放开,受认证和治理策略控制。

开发者最容易忽略的协议细节

问题正确理解
Webhook 和 Polling 能否同时作为主链路通常不建议。启用 Webhook 后,Polling 可能拿不到更新。
群里给 Bot 发普通消息为什么不触发默认是 mention_only,不是系统故障。
签名是否能用解析后的 JSON 再计算不建议。应使用统一 canonical JSON 规则。
收到同一 Update 两次是不是平台 bug不是。接入方必须做去重。
parse_mode 是否已是稳定主路径还不是。当前建议开发者直接显式提供 data.entities