# 当 webhook 一直没有送达

Source: https://help.didit.me/zh/integration/webhook-troubleshooting
Updated: 2026-10-12

按顺序排查：投递日志、事件名称、防火墙、签名。Deliveries 标签页会告诉你 Didit 是否发送过它，这一步能立刻把问题范围缩小一半。

先从目的地的 **Deliveries** 标签页看起。
如果 Didit 从未尝试投递，那是订阅或目的地配置的问题。
如果尝试过但失败了，响应码会告诉你原因：**404** 表示你的路由在该 URL 上不可达，签名不匹配表示你哈希了错误的字节内容。

## 第一步：Didit 是否尝试发送过

打开开发者和 Webhooks，选择端点，在投递列表查看每次请求和响应。修复后重新发送。检查应用、环境、事件订阅，并确认端点已启用。

![Didit 控制台中的 webhook 页面，显示各个目的地的投递状态](https://help.didit.me/screenshots/webhooks-light.webp)

*判断是事件缺失还是端点故障，所需信息都在这个页面上。*

这一个检查就能把问题分成两半：

- **没有记录到任何尝试** → 该目的地从未生成过这个事件。前往第二步。
- **记录到尝试，但失败了** → Didit 发送过，但你这边拒绝了它，或者没有收到。前往第三步。
- **记录到尝试，且是 2xx** → 投递成功了。问题出在你的处理逻辑里，而不是投递环节。

## 第二步：没有记录到任何尝试

按可能性从高到低排列：

1. **事件没有被订阅。** 没有通配符，每个事件系列都必须显式列出。一个不存在的事件名（例如 `session.status.updated`、`kyc.completed`）会静默地什么都收不到。对照[事件列表](https://help.didit.me/zh/integration/webhooks-basics)检查一下。
2. **应用不对。** 目的地属于某个应用。如果你的会话运行在与该目的地不同的应用下，事件永远不会到达。这是配置看起来一切正常时最常见的原因。
3. **确实没有变化。** webhook 只在变化时触发。一个没有移动过的会话，或者一次没有发现超过阈值命中的 AML 重新筛查，正确地不会产生事件。
4. **你等待的状态还没有发生。** 处于进行中的会话还没有完成。参见[当会话一直没有完成](https://help.didit.me/zh/verification-sessions/session-not-finished)。

## 第三步：尝试过投递但失败了

**404** 表示请求到达了某处，但那里没有你的路由。请检查：

- 完整路径是否正确，包括结尾的斜杠。会把 `/hook` 重定向到 `/hook/` 的框架，可能把一个正常工作的端点变成 404 或丢失请求体。
- URL 是否可公开访问。本地或预发布环境如果无法从公网访问，就会出现这种失败。
- 应用前面的代理、负载均衡器或基于路径的路由，是否把这个路径转发到了别处。

**5xx** 表示你的处理逻辑抛出了异常。在解析之前先记录原始请求体，这样你才能看到它实际收到了什么。

**超时** 表示你没有及时响应。应先返回 2xx，再进行处理。

**完全没有响应 / 连接被拒绝** 表示你的边界拦截了它。
Didit 从静态 IP **18.203.201.92** 发送请求，User-Agent 为 `DiditWebhook/2.0`。
如果你在 Cloudflare 或采用默认拒绝策略的 WAF 后面，请为接收主机名放行这个 IP。

## 常见情况：自动投递 404，但 Resend 却成功

这种情况足够常见，值得单独说明。
自动投递以 404 失败，随后在同一事件上点击 **Resend** 却成功了。

出现这种组合说明负载和你的端点都没有问题，差异在于时机或路径，而不是内容。请检查：

- **发生原始投递那一刻正好在部署或重启。** 之后 Resend 之所以成功，是因为应用已经恢复运行。
- **冷启动超出了平台的超时时间**，这在无服务器架构且首次调用较慢时很常见。
- **两次尝试之间基于路径的路由发生了变化**，或者某条规则只匹配部分请求。
- **边界的速率限制或机器人防护**，让单独到达的手动重发通过了，而突发到达的自动投递没有通过。

Deliveries 标签页记录了两次尝试各自的时间戳，可以拿它们和你自己那一分钟的部署与错误日志做对比。

## 签名验证失败

几乎总是以下三种情况之一：

1. **你对重新序列化后的 JSON 做了哈希。** 应该对**原始**请求体字节做 HMAC，完全按接收到的样子，而不是解析后再重新字符串化的版本。解析再重新拼接会改变空白和键的顺序，签名就会对不上。大多数框架需要显式配置才能拿到原始请求体。
2. **密钥不对。** 签名密钥是**按目的地**分配的，并不是你的 API 密钥。两个目的地会有两个不同的密钥。
3. **编码方式假设错误。** 如果你不确定密钥应该按字面字符串使用还是需要先解码，不要猜测，严格按照[签名验证参考文档](https://docs.didit.me/integration/webhooks#signature-verification)操作，并在调试时记录原始请求体以便对比。

> **Warning:** 永远不要通过跳过验证来"修复"签名不匹配的问题。
> 一个未经验证的 webhook 端点，会接受任何找到该 URL 的人伪造的批准信息，这会把一个调试上的权宜之计变成账号接管的攻击路径。

## 两次重试不等于一个队列

收到 5xx 或 404 响应时，Didit 会在约一分钟后重试，再过四分钟重试一次。超过 5 秒的超时或连接失败不会自动重试。修复后在投递列表使用重新发送；最终失败会产生提醒。

请搭建一条对账路径：在启动时，针对所有没有终态状态记录的会话轮询决策接口。
把 webhook 当作快速通道，把轮询当作兜底方案。

## 无需运行验证即可测试

[Video: 设置 API 密钥并测试您的 Webhook - Didit Academy](https://www.youtube.com/watch?v=thOMIGbNLP0&t=140)

目的地页面上的 **发送测试事件** 会发送一个你选择类型的完整事件（已批准、已拒绝、审核中、KYB、实体、交易等场景）到你的端点。
用它可以在真实会话依赖它之前，验证你的端点、签名校验和处理逻辑是否正常工作。

沙盒是另外一半：沙盒会话会发出带有 `"environment": "sandbox"` 的真实 webhook，因此你可以免费端到端地演练整条路径。
参见[在沙盒中测试](https://help.didit.me/zh/getting-started/sandbox-and-testing)。
