接收 Webhook 并验证签名
Ody 将工作区事件——新消息、呼叫结果、联系人更改——推送到您通过 API 注册的 HTTPS 端点,每次交付都按照 Standard Webhooks 规范进行签名,以便您可以证明它来自 Ody。
开始之前
- 您需要一个具有
webhooks:manage范围的 API 密钥——请参阅获取您的 Ody API 密钥。 - 您的端点必须是 HTTPS URL,并能快速响应
2xx状态(交付在 10 秒后超时)。 - 每个工作区最多可以注册 50 个端点。
分步指南
- 列出您可以订阅的事件类型:
curl https://api.ody.co/v1/api/webhook-events \
-H "Authorization: Bearer ody_live_…"
目前的目录包括:message.received、message.delivered、message.failed、call.ringing、call.answered、call.completed、call.forwarded、call.missed、call.recording.completed、call.summary.completed、call.transcript.completed、call.voicemail.completed、call.flow.started、call.flow.completed、call.flow.aborted、contact.updated、contact.deleted。订阅 "*" 将涵盖所有当前和未来的事件类型。
call.flow.* 事件在呼叫由呼叫流程接听时触发:当呼叫者进入流程时触发 started,当他们到达目的地(目的地类型包括——响铃、AI 助手、语音邮件、转接或挂断)时触发 completed,如果流程无法完成则触发 aborted。
- 注册您的端点:
curl https://api.ody.co/v1/api/webhooks \
-H "Authorization: Bearer ody_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/ody-webhook","events":["message.received"],"label":"prod"}'
- 立即存储响应中的
secret。whsec_…签名密钥仅在创建和轮换时返回——此后将不再显示。 - 向自己发送一个签名的测试事件,并确认您的端点已验证并接受它:
curl -X POST https://api.ody.co/v1/api/webhooks/WEBHOOK_ID/test \
-H "Authorization: Bearer ody_live_…"
测试会发布一个 {"type":"ping"} 事件,其签名方式与实际交付完全相同。
交付示例
每次交付都是一个带有 JSON 主体和三个签名标头的 POST 请求:
{
"id": "evt_…",
"apiVersion": "2026-06-01",
"createdAt": "2026-08-16T12:00:00.000Z",
"type": "message.received",
"data": { "conversationId": "…", "activityId": "…", "numberE164": "+1…" }
}
| 标头 | 含义 |
|---|---|
webhook-id |
事件 ID — 用于去重,因为重试会复用它 |
webhook-timestamp |
发送时的 Unix 秒数 |
webhook-signature |
对 <id>.<timestamp>.<raw body> 进行 v1,<base64 HMAC-SHA256> 签名 |
验证签名
HMAC 密钥是您的密钥在 whsec_ 前缀之后的 base64 解码部分,签名消息是 <webhook-id>.<webhook-timestamp>.<raw body>。始终根据原始请求字节进行验证——重新序列化解析后的 JSON 会破坏签名。拒绝几分钟前的旧时间戳以阻止重放攻击。
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
app.post("/ody-webhook", express.raw({ type: "application/json" }), (req, res) => {
const secret = process.env.ODY_WEBHOOK_SECRET; // whsec_…
const id = req.headers["webhook-id"];
const timestamp = req.headers["webhook-timestamp"];
const signature = req.headers["webhook-signature"];
// Replay guard: reject deliveries older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = "v1," + createHmac("sha256", key)
.update(`${id}.${timestamp}.${req.body}`)
.digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(String(signature));
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
console.log("verified event:", event.type);
res.status(200).end();
});
官方 SDK 将此功能封装为一行代码——verifyWebhookSignature (TypeScript) 和 verify_webhook_signature (Python),两者都内置了常数时间比较和 5 分钟重放保护。请参阅SDK 快速入门:TypeScript 和 Python。
重试、历史记录和管理
- 重试: 失败的交付(非 2xx、超时或连接错误)将在第一次尝试后大约 5 秒和 30 秒后重试——总共三次尝试,每次超时 10 秒。
- 历史记录:
GET /v1/api/webhooks/:id/deliveries列出最新的交付记录,包括每次尝试的状态码、错误和持续时间——当事件似乎丢失时,这是您的首选检查点。 - 管理端点:
GET /v1/api/webhooks列出所有端点(不包括密钥),PATCH /v1/api/webhooks/:id更新url、events、label或status(enabled/disabled),DELETE /v1/api/webhooks/:id删除一个端点及其交付历史记录。 - 随时轮换密钥,使用
POST /v1/api/webhooks/:id/rotate——新的whsec_…只返回一次,旧签名将立即停止验证。
故障排除
- 签名验证总是失败——您可能正在验证一个重新序列化的主体。请使用原始请求字节,并确认您在去除
whsec_后对密钥进行了 base64 解码。 - 没有交付到达——检查端点的
status是否为enabled,其events列表是否包含您期望的类型(或"*"),并查看交付历史记录以查找每次尝试的错误。 - 重复事件——重试会复用相同的
webhook-id。请根据它进行去重。 webhook limit reached——您已达到工作区的 50 个端点限制;请先删除一个未使用的端点。
相关文章
常见问题
Webhook 交付是如何签名的?
根据 Standard Webhooks 规范:`webhook-signature` 标头格式为 `v1,<base64 HMAC-SHA256>`,使用您的 `whsec_` 密钥对 `'<webhook-id>.<webhook-timestamp>.<raw body>'` 计算得出。
如果我的端点宕机了怎么办?
Ody 会重试两次——第一次失败后大约 5 秒和 30 秒。请查看 `GET /v1/api/webhooks/:id/deliveries` 以获取每次尝试的状态码和错误信息。
我丢失了签名密钥——现在怎么办?
使用 `POST /v1/api/webhooks/:id/rotate` 轮换它。新的 `whsec_` 密钥只会返回一次;旧签名将立即失效。