ট্র্যাকিং
সিগনেচার সহ ডেলিভারি Webhooks
কনসোলের Webhooks পৃষ্ঠায় একটি 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-এনকোড করুন এবং ধ্রুব সময়ে সিগনেচারের সাথে তুলনা করুন। আপনার ঘড়ি থেকে পাঁচ মিনিটের বেশি দূরের টাইমস্ট্যাম্পগুলি প্রত্যাখ্যান করুন। সর্বদা 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);# 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 ঘন্টা।
- প্রতিটি প্রচেষ্টা Webhooks পেজে HTTP স্ট্যাটাস এবং সময়সহ তালিকাভুক্ত করা হয়।
ডুপ্লিকেট এবং অর্ডার
একটি ইভেন্ট দুবার আসতে পারে, উদাহরণস্বরূপ যখন আপনার এন্ডপয়েন্ট এটি সংরক্ষণ করেছে কিন্তু উত্তর হারিয়ে গেছে। webhook-id সংরক্ষণ করুন এবং আপনি ইতিমধ্যে হ্যান্ডেল করা আইডিগুলি এড়িয়ে যান। বিভিন্ন মেসেজের ইভেন্টগুলি যেকোনো ক্রমে আসতে পারে; আগমনের ক্রমের উপর নির্ভর করার পরিবর্তে data.status এবং data.done_at পড়ুন।
এন্ডপয়েন্ট নিয়ম
- শুধুমাত্র HTTPS, একটি পাবলিক হোস্ট নামে (কোনো IP লিটারাল, localhost বা প্রাইভেট নেটওয়ার্ক নয়)।
- Webhooks পেজে যেকোনো সময় সিক্রেট পরিবর্তন করা যাবে; তারপর সাইন করা কলগুলি নতুন সিক্রেট ব্যবহার করবে।
- এন্ডপয়েন্ট সরালে নতুন ইভেন্ট বন্ধ হবে; ইতিমধ্যে শেষ হওয়া মেসেজগুলি পোলিংয়ের জন্য তাদের স্ট্যাটাস রাখে।