跟踪

带签名的送达Webhook

在控制台Webhook页面设置HTTPS接口。当消息达到最终状态时,我们会向该接口发送携带JSON事件的POST请求

事件类型

type时间
message.delivered消息已送达
message.failed消息发送失败,data.error 包含错误代码。费用已退回余额
POST to your endpointJSON
{
  "id": "evt_8F3K2Q9TZ1MXD",
  "type": "message.delivered",
  "created_at": "2026-11-07T09:30:17.902Z",
  "data": {
    "id": "msg_8F3K2Q9TZ1MX",
    "object": "message",
    "batch_id": "bat_4M7Q1X0PK2RS",
    "to": "+14155550123",
    "country": "US",
    "text": "Your Northwind code is 482913. It expires in 10 minutes.",
    "segments": 1,
    "price": "0.00507",
    "currency": "USD",
    "status": "delivered",
    "error": null,
    "reference": "login-7781",
    "created_at": "2026-11-07T09:30:12.418Z",
    "sent_at": "2026-11-07T09:30:13.020Z",
    "done_at": "2026-11-07T09:30:17.902Z"
  }
}

请求头与签名

每次调用都携带三个标准Webhook格式的请求头:

请求头阈值
webhook-id事件ID (evt_...)。同一事件的所有重试都使用相同的ID。
webhook-timestamp此次尝试签名时的Unix时间(秒)。
webhook-signaturev1,后跟base64 HMAC-SHA256签名。可能列出多个签名,以空格分隔:当任一签名匹配时接受调用。

验证调用:从控制台获取密钥(以whsec_开头),对whsec_之后的部分进行base64url解码以获取密钥,计算webhook-id + "." + webhook-timestamp + "." + raw body的HMAC-SHA256,进行base64编码并与签名在恒定时间内比较。拒绝与当前时间相差超过五分钟的时间戳。始终使用原始请求体:解析并重新序列化的JSON请求体将不匹配。

// Express: keep the raw body, check the signature, then answer 2xx quickly
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.BULKSMSAPI_WEBHOOK_SECRET; // whsec_...

app.post("/hooks/sms", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.header("webhook-id");
  const ts = req.header("webhook-timestamp");
  const key = Buffer.from(secret.slice("whsec_".length), "base64url");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${req.body}`).digest("base64");
  const sent = (req.header("webhook-signature") || "").split(" ").map((s) => s.replace(/^v1,/, ""));
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  if (!fresh || !sent.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.id, event.data.status);
  res.status(204).end();
});
app.listen(3000);

响应与重试

  • 在10秒内以任何2xx状态响应。响应后从队列中执行耗时任务。
  • 不跟随重定向:将设置指向最终地址。
  • 超时或返回非2xx状态的调用将在30秒、2分钟、10分钟、30分钟、1小时、2小时、4小时、8小时、8小时后重试:总共10次尝试,从第一次开始大约24小时。
  • 每次尝试都会在Webhooks页面上列出,包括HTTP状态和耗时。

重复与顺序

事件可能会重复到达,例如当您的端点已保存但响应丢失时。存储webhook-id并跳过已处理的ID。不同消息的事件可能以任意顺序到达;读取data.status和data.done_at,而不是依赖到达顺序。

接口规范

  • 仅支持HTTPS,且使用公共主机名(不支持IP地址、localhost或私有网络)。
  • 可以在Webhooks页面随时替换密钥;之后的调用将使用新密钥签名。
  • 移除端点将停止新事件;已完成的消息保留其状态以供轮询。
带签名的送达Webhook | BulkSMSAPI文档