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']);
}