投递行为
取自该平台的官方文档,最近核对于 2026-08-06。平台没有给出数字的地方,这张表也不填。
| 响应时限 | 15 秒 可配置。rt 读取超时默认 15000 毫秒(最大 15000),ct 连接超时默认 5000 毫秒(最大 10000),tt 总超时默认 15000 毫秒 |
|---|---|
| 重试 | 可配置:rc 取 0 到 5,默认 1。rp 指定哪些失败可以重试 |
| 放弃 | 平台未公布 |
| 出处 | https://www.twilio.com/docs/usage/webhooks/webhooks-connection-overrides |
签名
用一个经测试套件证明的可运行验证器核对,不是照着文档读来的。最近核对于 2026-08-20。
| 算法 | HMAC-SHA1 |
|---|---|
| 待签名串 | full URL + form params sorted by key |
| 编码 | base64 |
| 请求头 | x-twilio-signature |
| 时间容差 | 无强制要求 |
验证签名
要用原始请求体,逐字节一致,并且在任何 JSON 解析之前。框架一旦重新序列化请求体,本页的每一种方案都会失效。
const crypto = require("node:crypto");
// base64(HMAC-SHA1(auth token, url + form params sorted by key, concatenated))
const sorted = Object.keys(req.body).sort();
const base = fullUrl + sorted.map((k) => k + req.body[k]).join("");
const expected = crypto
.createHmac("sha1", authToken)
.update(base)
.digest("base64");
const received = Buffer.from(req.headers["x-twilio-signature"] ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
received.length === wanted.length && crypto.timingSafeEqual(received, wanted);签名覆盖完整的公网 URL,包括查询字符串。处理程序前面的 TLS 终止或改写路径的代理,会改变 Twilio 签名时的 URL,签名对不上时看起来和伪造请求一模一样。
容易踩的坑
每个 URL 都可以配置五个投递参数:连接超时、读取超时、总时长、重试次数、重试策略。而几乎所有人用的都是默认值(总共 15 秒,重试 1 次)。旋钮就在那里,没人去拧。
延伸阅读
AnyHook 挡在接收 Twilio Webhook 的端点前面:约 1 秒应答,你的服务器宕机时按自己的计划重试,并且每个事件都可以重放。 只改一个 URL,代码不用动。