追蹤

帶簽名的送達 webhooks

在控制台的 Webhooks 頁面設定 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"
  }
}

標頭與簽名

每次呼叫都帶有三個標頭,符合 Standard Webhooks 格式:

標頭數值
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 頁面上隨時更換;之後簽署的呼叫使用新密鑰。
  • 移除端點會停止新事件;已經完成的訊息保留其狀態以供輪詢。
帶簽名的送達 webhooks | BulkSMSAPI 文檔