ट्रैकिंग
सिग्नेचर के साथ डिलीवरी वेबहुक
कंसोल के वेबहुक पेज पर एक HTTPS एंडपॉइंट सेट करें। जब कोई संदेश अंतिम स्थिति तक पहुँचता है, तो हम उस एंडपॉइंट पर एक JSON इवेंट के साथ एक POST भेजते हैं।
इवेंट्स
| type | जब |
|---|---|
| message.delivered | संदेश डिलीवर कर दिया गया था। |
| message.failed | संदेश विफल हुआ; data.error में कोड है। इसकी कीमत वापस कर दी गई है। |
{
"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-timestamp | यूनिक्स टाइम (सेकंड में) जब यह प्रयास साइन किया गया था। |
| webhook-signature | v1, उसके बाद 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);# 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))
}उत्तर और रिट्राइज़
- 10 सेकंड के भीतर कोई भी 2xx स्टेटस के साथ उत्तर दें। धीमा काम उत्तर देने के बाद, कतार से करें।
- रिडायरेक्ट्स को फॉलो नहीं किया जाता: सेटिंग को फाइनल एड्रेस पर पॉइंट करें।
- टाइमआउट होने वाली या 2xx के अलावा किसी अन्य स्टेटस वाली कॉल को 30 सेकंड, 2 मिनट, 10 मिनट, 30 मिनट, 1 घंटा, 2 घंटे, 4 घंटे, 8 घंटे, 8 घंटे बाद फिर से ट्राई किया जाता है: कुल 10 प्रयास, पहले प्रयास से लगभग 24 घंटे तक।
- प्रत्येक प्रयास वेबहुक्स पेज पर HTTP स्टेटस और लिया गया समय के साथ सूचीबद्ध होता है।
डुप्लिकेट्स और क्रम
एक इवेंट दो बार आ सकता है, उदाहरण के लिए जब आपके एंडपॉइंट ने इसे सेव कर दिया हो लेकिन उत्तर खो गया हो। webhook-id को स्टोर करें और पहले से हैंडल किए गए आईडी को स्किप करें। अलग-अलग मैसेज के इवेंट किसी भी क्रम में आ सकते हैं; आने के क्रम पर भरोसा करने के बजाय data.status और data.done_at पढ़ें।
एंडपॉइंट नियम
- केवल HTTPS, पब्लिक होस्ट नाम पर (कोई IP लिटरल, localhost या प्राइवेट नेटवर्क नहीं)।
- सीक्रेट को वेबहुक्स पेज पर किसी भी समय बदला जा सकता है; उसके बाद साइन की गई कॉल नए सीक्रेट का उपयोग करेंगी।
- एंडपॉइंट हटाने से नए इवेंट रुक जाते हैं; पहले से पूर्ण हुए मैसेज अपना स्टेटस पोलिंग के लिए रखते हैं।