Webhooks
Get real-time updates instead of polling. Each event is POSTed as JSON to your HTTPS URL.
Setup
bash
curl -X POST https://smsprovider-web.vercel.app/api/v1/webhooks \
-H "Authorization: Bearer $BATLER_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/batler", "events": ["message.delivered", "message.failed", "inbound.received"]}'The response includes a secret (whsec_…) — shown only once. Store it to verify signatures.
Events
| Event | When |
|---|---|
message.sent | Handed to the mobile network |
message.delivered | Delivery receipt from the handset |
message.failed | Failure, with reason (e.g. blocked, no_coverage, kosher_phone) |
message.expired | No delivery receipt in time |
inbound.received | A reply to your sender number |
otp.verified / otp.failed | OTP verification outcome |
wallet.low_balance / wallet.credited | Low balance / credits purchased |
blacklist.added | A recipient opted out |
link.clicked | A tracked link was clicked |
Shape:
json
{
"id": "4f0c7c1e-1c2b-4a0e-9d7b-2e3f4a5b6c7d",
"type": "message.delivered",
"created_at": "2026-11-02T18:00:05Z",
"data": { "message_id": "0e9b2f0a-6c1d-4f43-8c33-6f3b3f0d9a11", "to": "+972501234567", "ref": "order-123", "status": "delivered", "reason": null, "operator": "Partner", "occurred_at": "2026-11-02T18:00:04Z" }
}Verifying the signature
Every request carries X-Batler-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"), plus X-Batler-Event-Id for deduplication. Compute the signature over the raw body and reject timestamps older than 5 minutes.
Node.js
ts
import { verifyWebhookSignature } from "@batler/sms";
app.post("/hooks/batler", express.raw({ type: "application/json" }), async (req, res) => {
const raw = req.body.toString("utf8");
if (!(await verifyWebhookSignature(process.env.BATLER_WEBHOOK_SECRET!, req.get("X-Batler-Signature"), raw))) return res.sendStatus(401);
const event = JSON.parse(raw);
// dedupe by req.get("X-Batler-Event-Id"), then handle event.type
res.sendStatus(200);
});PHP
php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BATLER_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $p);
$expected = hash_hmac('sha256', $p['t'] . '.' . $raw, getenv('BATLER_WEBHOOK_SECRET'));
if (!isset($p['v1']) || !hash_equals($expected, $p['v1']) || abs(time() - (int)$p['t']) > 300) {
http_response_code(401); exit;
}
$event = json_decode($raw, true);
http_response_code(200);Python
python
import hmac, hashlib, time
def verify(secret: str, header: str, raw: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Retries
Return 2xx within 10 seconds. Otherwise we retry after 1m, 5m, 30m, 2h, 6h and 24h. After 10 consecutive failures the endpoint is disabled and you get an email. Delivery log and replay: GET https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries and …/replay.