トラッキング

署名付き配信Webhooks

コンソールのWebhooksページでHTTPSエンドポイントを設定。メッセージが最終ステータスになると、JSONイベントを含むPOSTを1回送信します

イベント

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形式で3つのヘッダーが付与されます:

ヘッダー値
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エンコードして署名と一定時間で比較します。5分以上ずれたタイムスタンプは拒否します。常に生のボディを使用してください:パースして再シリアライズされた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時間かかります。
  • すべての試行は、HTTPステータスと所要時間とともにWebhooksページにリストされます。

重複と順序

イベントが二重に届く場合があります(例:エンドポイントが保存したが応答が失われた場合)。webhook-idを保存し、処理済みのIDはスキップしてください。異なるメッセージのイベントは任意の順序で到着するため、到着順序ではなくdata.statusとdata.done_atを参照してください。

エンドポイント規則

  • HTTPSのみ、公開ホスト名で(IPリテラル、localhost、プライベートネットワークは不可)。
  • 秘密鍵はWebhooksページでいつでも変更可能です。変更後に署名されたコールは新しい秘密鍵を使用します。
  • エンドポイントを削除すると新しいイベントが停止しますが、完了済みメッセージはポーリング用にステータスを保持します。
署名付き配信Webhooks | BulkSMSAPI ドキュメント