Webhooks
Add one in the console (Projects → your project → Webhooks) or with the API:
curl https://sms.bizmakers.app/v1/webhooks -H "Authorization: Bearer $ESEMES_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/sms", "events": ["message.sent", "message.delivered", "message.failed", "message.received"]}'
The answer contains secret (shown once). URLs must be public https.
Events: message.queued, message.dispatched, message.sent, message.delivered, message.failed,
message.expired, message.canceled, message.retried, message.received (a reply).
What you receive
POST with JSON:
{
"id": 4211,
"type": "message.delivered",
"created_at": "2026-10-03T08:15:02Z",
"data": {"device_id": "…"},
"message": {"id": "…", "to": "+23051234567", "state": "delivered", "kind": "transactional",
"error_code": null, "metadata": {"order": 1042}, "test": false, "direction": "outbound"}
}
Headers: X-SMS-Event (the type), X-SMS-Delivery (unique per delivery, use it to ignore duplicates),
X-SMS-Signature: t=<unix seconds>,v1=<hex>. OTP codes are never included.
Answer 2xx within 10 seconds. Anything else is retried after 10 s, 30 s, 2 min, 10 min, 30 min, 2 h, 6 h, 12 h, 12 h,
24 h, then given up. The console shows every delivery and can replay it, and Send test event sends a ping.
Check the signature (always)
v1 = HMAC-SHA256(secret, t + "." + raw request body), hex. Reject when it does not match or t is more than
5 minutes old. Use the raw body, before any JSON parsing.
Node.js
import crypto from "node:crypto"
export function verify(rawBody, header, secret) {
const [t, v1] = header.split(",").map((p) => p.split("=")[1])
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
return v1.length === want.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(want))
}
Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
want = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts["v1"])
PHP
function esemes_verify(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int)$p['t']) > 300) return false;
$want = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($want, $p['v1']);
}