Pagsubaybay
Delivery webhooks na may mga lagda
Magtakda ng HTTPS endpoint sa Webhooks page ng console. Kapag ang isang mensahe ay umabot sa final status, magpapadala kami ng isang POST na may JSON event sa endpoint na iyon.
Mga Event
| type | Kailan |
|---|---|
| message.delivered | Naipahatid ang mensahe. |
| message.failed | Nabigo ang mensahe; ang data.error ay may code. Ang presyo nito ay ibinalik na. |
{
"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"
}
}Mga Header at lagda
Bawat tawag ay may tatlong header, sa Standard Webhooks format:
| Header | Halaga |
|---|---|
| webhook-id | Ang event id (evt_...). Ang parehong id ay ginagamit para sa bawat retry ng isang event. |
| webhook-timestamp | Unix time sa segundo nang lagdaan ang attempt na ito. |
| webhook-signature | v1, kasunod ng base64 HMAC-SHA256 signature. Maraming signature ang maaaring ilista, pinaghihiwalay ng mga espasyo: tanggapin ang tawag kapag may tumugma. |
Para i-check ang tawag: kunin ang secret mula sa console (nagsisimula ito sa whsec_), base64url-decode ang bahagi pagkatapos ng whsec_ para makuha ang key, kalkulahin ang HMAC-SHA256 ng webhook-id + "." + webhook-timestamp + "." + raw body, i-base64-encode ito at ihambing ito sa signature sa pare-parehong oras. Tanggihan ang mga timestamp na higit sa limang minuto ang layo mula sa iyong orasan. Laging gamitin ang raw body: ang isang parsed at re-serialised na JSON body ay hindi magtutugma.
// 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);# Flask: check the signature on the raw body before parsing it
import base64, hashlib, hmac, json, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["BULKSMSAPI_WEBHOOK_SECRET"] # whsec_...
@app.post("/hooks/sms")
def sms_hook():
msg_id = request.headers.get("webhook-id", "")
ts = request.headers.get("webhook-timestamp", "0")
raw = request.get_data()
key = base64.urlsafe_b64decode(SECRET[len("whsec_"):] + "==")
signed = msg_id.encode() + b"." + ts.encode() + b"." + raw
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
sent = [s.split(",", 1)[-1] for s in request.headers.get("webhook-signature", "").split()]
if abs(time.time() - int(ts)) > 300 or not any(hmac.compare_digest(s, expected) for s in sent):
abort(401)
event = json.loads(raw)
print(event["type"], event["data"]["id"], event["data"]["status"])
return "", 204<?php
// Plain PHP endpoint, for example /hooks/sms.php
$secret = getenv("BULKSMSAPI_WEBHOOK_SECRET"); // whsec_...
$raw = file_get_contents("php://input");
$id = $_SERVER["HTTP_WEBHOOK_ID"] ?? "";
$ts = $_SERVER["HTTP_WEBHOOK_TIMESTAMP"] ?? "0";
$key = base64_decode(strtr(substr($secret, 6), "-_", "+/"));
$expected = base64_encode(hash_hmac("sha256", "$id.$ts.$raw", $key, true));
$ok = false;
foreach (explode(" ", $_SERVER["HTTP_WEBHOOK_SIGNATURE"] ?? "") as $part) {
$sig = substr($part, strpos($part, ",") + 1);
if (hash_equals($expected, $sig)) $ok = true;
}
if (!$ok || abs(time() - (int)$ts) > 300) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
error_log($event["type"] . " " . $event["data"]["id"]);
http_response_code(204);package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func smsHook(w http.ResponseWriter, r *http.Request) {
secret := os.Getenv("BULKSMSAPI_WEBHOOK_SECRET") // whsec_...
key, _ := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
raw, _ := io.ReadAll(r.Body)
id, ts := r.Header.Get("webhook-id"), r.Header.Get("webhook-timestamp")
mac := hmac.New(sha256.New, key)
mac.Write([]byte(id + "." + ts + "." + string(raw)))
expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
sec, _ := strconv.ParseInt(ts, 10, 64)
ok := false
for _, part := range strings.Fields(r.Header.Get("webhook-signature")) {
sig := part[strings.Index(part, ",")+1:]
if hmac.Equal([]byte(sig), []byte(expected)) {
ok = true
}
}
if !ok || math.Abs(float64(time.Now().Unix()-sec)) > 300 {
w.WriteHeader(http.StatusUnauthorized)
return
}
var event struct {
Type string
Data map[string]any
}
json.Unmarshal(raw, &event)
log.Println(event.Type, event.Data["id"], event.Data["status"])
w.WriteHeader(http.StatusNoContent)
}
func main() {
http.HandleFunc("/hooks/sms", smsHook)
log.Fatal(http.ListenAndServe(":3000", nil))
}Pagsagot at pagsubok ulit
- Sumagot ng anumang 2xx status sa loob ng 10 segundo. Gawin ang mabagal na trabaho pagkatapos sumagot, mula sa isang queue.
- Hindi sinusundan ang mga redirect: ituro ang setting sa pangwakas na address.
- Ang isang tawag na nag-time out o sumagot ng anumang bagay maliban sa 2xx ay susubukan muli pagkatapos ng 30 segundo, 2 minuto, 10 minuto, 30 minuto, 1 oras, 2 oras, 4 oras, 8 oras, 8 oras: 10 attempts sa kabuuan, mga 24 oras mula sa una.
- Ang bawat attempt ay nakalista sa Webhooks page kasama ang HTTP status at ang oras na kinuha nito.
Mga Duplicate at pagkakasunod-sunod
Ang isang event ay maaaring dumating nang dalawang beses, halimbawa kapag na-save ito ng iyong endpoint ngunit nawala ang sagot. Itago ang webhook-id at laktawan ang mga id na iyong na-handle na. Ang mga event para sa iba't ibang mensahe ay maaaring dumating sa anumang pagkakasunud-sunod; basahin ang data.status at data.done_at sa halip na umasa sa pagkakasunud-sunod ng pagdating.
Mga Tuntunin sa Endpoint
- HTTPS lamang, sa isang public host name (walang IP literals, localhost o private networks).
- Ang secret ay maaaring palitan sa Webhooks page anumang oras; ang mga tawag na nilagdaan pagkatapos nito ay gagamit ng bagong secret.
- Ang pag-alis ng endpoint ay humihinto sa mga bagong event; ang mga mensahe na natapos na ay pinapanatili ang kanilang status para sa polling.