ติดตาม

Webhooks การส่งพร้อมลายเซ็น

ตั้งค่า HTTPS endpoint ในหน้า Webhooks ของคอนโซล เมื่อข้อความถึงสถานะสุดท้าย เราจะส่ง POST หนึ่งครั้งพร้อมเหตุการณ์ JSON ไปยัง endpoint นั้น

เหตุการณ์

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รหัสเหตุการณ์ (evt_...). รหัสเดียวกันนี้จะถูกใช้สำหรับการลองใหม่ทุกครั้งของเหตุการณ์หนึ่ง
webhook-timestampUnix time ในหน่วยวินาทีเมื่อการลองครั้งนี้ถูกเซ็น
webhook-signaturev1, ตามด้วยลายเซ็น base64 HMAC-SHA256 อาจมีลายเซ็นหลายรายการแยกด้วยช่องว่าง: ยอมรับการเรียกเมื่อใดก็ตามที่ตรงกัน

เพื่อตรวจสอบการเรียก: นำคีย์ลับจากคอนโซล (เริ่มต้นด้วย whsec_), base64url-decode ส่วนที่อยู่หลัง whsec_ เพื่อรับคีย์, คำนวณ HMAC-SHA256 ของ webhook-id + "." + webhook-timestamp + "." + raw body, base64-encode และเปรียบเทียบกับลายเซ็นในเวลาคงที่ ปฏิเสธ timestamp ที่ห่างจากนาฬิกาของคุณเกินห้านาที ใช้ raw body เสมอ: JSON body ที่ถูกแยกและจัดเรียงใหม่จะไม่ตรงกัน

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

การตอบสนองและการลองใหม่

  • ตอบกลับด้วยสถานะ 2xx ใดๆ ภายใน 10 วินาที ทำงานที่ใช้เวลานานหลังจากตอบกลับ จากคิว
  • ไม่ทำตามการเปลี่ยนเส้นทาง: ชี้การตั้งค่าไปที่ที่อยู่สุดท้าย
  • การเรียกที่หมดเวลาหรือตอบกลับด้วยอะไรก็ตามที่ไม่ใช่ 2xx จะถูกลองอีกครั้งหลังจาก 30 วินาที, 2 นาที, 10 นาที, 30 นาที, 1 ชั่วโมง, 2 ชั่วโมง, 4 ชั่วโมง, 8 ชั่วโมง, 8 ชั่วโมง: ทั้งหมด 10 ครั้ง ประมาณ 24 ชั่วโมงจากครั้งแรก
  • ทุกครั้งที่ลองจะถูกแสดงในหน้า Webhooks พร้อมกับสถานะ HTTP และเวลาที่ใช้

รายการซ้ำและลำดับ

เหตุการณ์อาจมาถึงสองครั้ง เช่น เมื่อ endpoint ของคุณบันทึกไว้ แต่คำตอบหายไป จัดเก็บ webhook-id และข้าม id ที่คุณจัดการไปแล้ว เหตุการณ์สำหรับข้อความต่าง ๆ อาจมาถึงในลำดับใดก็ได้ อ่าน data.status และ data.done_at แทนที่จะพึ่งพาลำดับการมาถึง

กฎของ endpoint

  • HTTPS เท่านั้น บนชื่อโฮสต์สาธารณะ (ไม่ใช่ IP literal, localhost หรือเครือข่ายส่วนตัว)
  • คีย์ลับสามารถถูกแทนที่ในหน้า Webhooks ได้ตลอดเวลา การเรียกที่เซ็นหลังจากนั้นจะใช้คีย์ลับใหม่
  • การลบ endpoint จะหยุดเหตุการณ์ใหม่ ข้อความที่เสร็จสิ้นแล้วจะคงสถานะไว้สำหรับการ polling
Webhooks การส่งพร้อมลายเซ็น | BulkSMSAPI Docs