OdyOdy 帮助

接收 Webhook 并验证签名

更新于 Sun Aug 30 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

Ody 将工作区事件——新消息、呼叫结果、联系人更改——推送到您通过 API 注册的 HTTPS 端点,每次交付都按照 Standard Webhooks 规范进行签名,以便您可以证明它来自 Ody。

开始之前

  • 您需要一个具有 webhooks:manage 范围的 API 密钥——请参阅获取您的 Ody API 密钥
  • 您的端点必须是 HTTPS URL,并能快速响应 2xx 状态(交付在 10 秒后超时)。
  • 每个工作区最多可以注册 50 个端点

分步指南

  1. 列出您可以订阅的事件类型:
curl https://api.ody.co/v1/api/webhook-events \
  -H "Authorization: Bearer ody_live_…"

目前的目录包括:message.receivedmessage.deliveredmessage.failedcall.ringingcall.answeredcall.completedcall.forwardedcall.missedcall.recording.completedcall.summary.completedcall.transcript.completedcall.voicemail.completedcall.flow.startedcall.flow.completedcall.flow.abortedcontact.updatedcontact.deleted。订阅 "*" 将涵盖所有当前和未来的事件类型。

call.flow.* 事件在呼叫由呼叫流程接听时触发:当呼叫者进入流程时触发 started,当他们到达目的地(目的地类型包括——响铃、AI 助手、语音邮件、转接或挂断)时触发 completed,如果流程无法完成则触发 aborted

  1. 注册您的端点:
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"}'
  1. 立即存储响应中的 secret whsec_… 签名密钥仅在创建和轮换时返回——此后将不再显示。
  2. 向自己发送一个签名的测试事件,并确认您的端点已验证并接受它:
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 更新 urleventslabelstatusenabled/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_` 密钥只会返回一次;旧签名将立即失效。

更多 开发者 API 内容