Theo dõi

Webhooks giao nhận có chữ ký

Thiết lập một endpoint HTTPS trên trang Webhooks của console. Khi tin nhắn đạt trạng thái cuối cùng, chúng tôi sẽ gửi một POST với sự kiện JSON đến endpoint đó.

Sự kiện

typeKhi nào
message.deliveredTin nhắn đã được gửi.
message.failedTin nhắn thất bại; data.error chứa mã lỗi. Số tiền đã được hoàn lại.
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"
  }
}

Tiêu đề và chữ ký

Mỗi cuộc gọi mang theo ba tiêu đề, theo định dạng Standard Webhooks:

Tiêu đềGiá trị
webhook-idID sự kiện (evt_...). ID này được sử dụng cho mọi lần thử lại của một sự kiện.
webhook-timestampThời gian Unix tính bằng giây khi lần thử này được ký.
webhook-signaturev1, theo sau là chữ ký base64 HMAC-SHA256. Có thể liệt kê nhiều chữ ký, phân cách bằng khoảng trắng: chấp nhận cuộc gọi khi bất kỳ chữ ký nào khớp.

Để kiểm tra một cuộc gọi: lấy secret từ console (bắt đầu bằng whsec_), giải mã base64url phần sau whsec_ để lấy khóa, tính toán HMAC-SHA256 của webhook-id + "." + webhook-timestamp + "." + raw body, mã hóa base64 và so sánh với chữ ký trong thời gian không đổi. Từ chối các timestamp cách xa đồng hồ của bạn hơn năm phút. Luôn sử dụng raw body: một JSON body đã được phân tích và tuần tự hóa lại sẽ không khớp.

// 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);

Phản hồi và thử lại

  • Trả lời với bất kỳ trạng thái 2xx nào trong vòng 10 giây. Thực hiện công việc chậm sau khi trả lời, từ một hàng đợi.
  • Không theo dõi chuyển hướng: chỉ định cài đặt đến địa chỉ cuối cùng.
  • Một cuộc gọi bị hết thời gian hoặc trả lời bất kỳ thứ gì khác ngoài 2xx sẽ được thử lại sau 30 giây, 2 phút, 10 phút, 30 phút, 1 giờ, 2 giờ, 4 giờ, 8 giờ, 8 giờ: tổng cộng 10 lần thử, khoảng 24 giờ từ lần đầu tiên.
  • Mọi lần thử đều được liệt kê trên trang Webhooks với trạng thái HTTP và thời gian thực hiện.

Bản sao và thứ tự

Một sự kiện có thể đến hai lần, ví dụ khi endpoint của bạn đã lưu nó nhưng câu trả lời bị mất. Lưu trữ webhook-id và bỏ qua các id bạn đã xử lý. Các sự kiện cho các tin nhắn khác nhau có thể đến theo bất kỳ thứ tự nào; đọc data.status và data.done_at thay vì dựa vào thứ tự đến.

Quy tắc endpoint

  • Chỉ HTTPS, trên một tên máy chủ công khai (không có IP literals, localhost hoặc mạng riêng).
  • Secret có thể được thay thế trên trang Webhooks bất kỳ lúc nào; các cuộc gọi được ký sau đó sẽ sử dụng secret mới.
  • Xóa endpoint sẽ dừng các sự kiện mới; các tin nhắn đã hoàn thành sẽ giữ nguyên trạng thái để polling.
Webhooks giao nhận có chữ ký | Tài liệu BulkSMSAPI