Webhooks
在您的后端接收会话事件:每次投递都带 HMAC-SHA256 签名,失败按指数退避重试,并附带唯一 id,方便接收端做幂等处理。
什么时候用 webhook
用来对房间里发生的事作出反应,而不必轮询:会话开始或结束、一道问题关闭、结果定稿时,平台会主动调用您。投递是至少一次的,可能重复,但绝不会悄无声息地失败,所以接收端必须做到幂等。
事件目录
由 API 在 /public/v1/limits 提供,本页不留副本。
- presentation.import.completed
- presentation.import.failed
- question.activated
- question.closed
- question.results.finalized
- report.ready
- session.ended
- session.paused
- session.resumed
- session.started
每次投递都带的请求头
X-Votinova-Event事件类型,与目录中的写法完全一致。X-Votinova-Delivery-Id每次投递唯一,也就是您的去重键。X-Votinova-TimestampEpoch 秒;它在签名范围之内,因此重放的投递无法被改成新时间。X-Votinova-Signature十六进制的 HMAC-SHA256,不带前缀。
请求体是紧凑的 JSON,包含事件和涉及的所有 id,下面这份就是那次带签名的示例投递,逐字节一致:
{"event":"session.ended","session_id":"68b2e3d4c5f607182930a5bc"}校验签名
每次投递都用端点密钥,对时间戳、一个点号和请求体做 HMAC-SHA256 签名。把结果与 X-Votinova-Signature 请求头比对即可。
- 请对原始请求体签名。解析后再序列化会改变空白字符,签名就对不上了。
- 请用常数时间比较,不要用 ==。
已用真实投递验证过
const crypto = require("node:crypto");
function verify(headers, rawBody, secret) {
const timestamp = headers["x-votinova-timestamp"];
const signature = headers["x-votinova-signature"];
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}secret whsec_3f8a1c92d4e5b6a7
timestamp 1785321600
body {"event":"session.ended","session_id":"68b2e3d4c5f607182930a5bc"}
HMAC-SHA256(secret, timestamp + "." + body)
= 10a86171bd92f5fcf8c28b873978e0214302cc2291c54cc016d18347f3ee7c38重试
Webhook 投递:最多尝试8次,等待间隔依次为21, 45, 95, 171, 369, 732, 1480秒。用尽之后投递会转入死信,可以手动重新投递。
幂等
请用 X-Votinova-Delivery-Id 作为去重键:同一次投递可能到达多次,而您的处理只应生效一次。
X-Votinova-Delivery-Id
最佳实践
- 确认投递已落地后就立刻返回 200,具体处理放进自己的队列,接收端慢会让重试堆积起来。
- 解析之前一定要先校验签名:否则,任何知道您 URL 的人都能伪造事件。
- 用 X-Votinova-Delivery-Id 去重:至少一次意味着同一次投递可能调用两次。
- 连续失败 10 次后,端点会自动暂停;接收端恢复后,在面板里或通过 API 重新启用它。