Webhooks push events to your server as they happen, so you do not have to poll. This is the recommended way to consume the API.
Register an endpoint
curl -X POST https://www.smsz.net/api/v1/webhooks/endpoints \
-H "Authorization: Bearer $SMSZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/smsz",
"events": ["activation.message.received", "rental.message.received"]
}'
The response contains a secret starting whsec_. This is the only time it is returned. Store it — it is what proves a delivery came from us.
Subscribe to ["*"] for everything, or use prefixes like ["rental.*"] for a family of events.
Payload shape
{
"id": "cmg7xh2k4000cl208a1b2c3d4",
"object": "event",
"type": "activation.message.received",
"api_version": "2026-07-01",
"created": 1784889404,
"data": {
"id": "cmg7x2k9a0001l208hq3v7bqz",
"object": "activation",
"status": "completed",
"phone_number": "+12025550147",
"messages": [
{ "id": "cmg7xb1s70006l208r9y2mnop", "object": "message", "sender": "Telegram", "text": "Telegram code 51284", "code": "51284", "received_at": "2026-07-24T10:16:44.000Z" }
]
}
}
data is the same object the REST endpoints return, so one deserializer handles both.
Verifying signatures
Every delivery carries a SMSZ-Signature header:
SMSZ-Signature: t=1784889404,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
v1 is HMAC-SHA256 of {timestamp}.{raw request body}, keyed with your endpoint secret. Verify it on the raw body, before any JSON parsing — re-serializing changes the bytes and the signature will not match.
import crypto from "node:crypto";
function verifySmszWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=").map((s) => s.trim()))
);
// Reject old deliveries so a captured payload cannot be replayed later.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// Express — note express.raw(), not express.json()
app.post("/hooks/smsz", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySmszWebhook(req.body.toString(), req.get("SMSZ-Signature"), process.env.SMSZ_WEBHOOK_SECRET)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body.toString());
// Acknowledge first, work afterwards: we retry anything that is not a 2xx.
res.status(200).send("ok");
handleEvent(event).catch(console.error);
});
import hmac, hashlib, time
from flask import Flask, request, abort
def verify_smsz_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in signature_header.split(","))
# Reject old deliveries so a captured payload cannot be replayed later.
if abs(time.time() - int(parts["t"])) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{parts['t']}.{raw_body.decode()}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
@app.post("/hooks/smsz")
def smsz_webhook():
if not verify_smsz_webhook(request.get_data(), request.headers["SMSZ-Signature"], SECRET):
abort(400)
event = request.get_json()
enqueue(event) # acknowledge fast, process out of band
return "", 200
Delivery rules
- Any 2xx is an acknowledgement. Anything else is retried.
- Retries: 8 attempts at 10s → 30s → 2m → 10m → 30m → 2h → 6h → 12h — just over 24 hours in total.
- Deliveries time out after 10 seconds, so acknowledge immediately and do the work asynchronously.
- Redirects are not followed. If your URL moves, update the endpoint.
- After 20 consecutive failures an endpoint is disabled. Re-enable it with
PATCH /webhooks/endpoints/{id} and {"status": "active"}. - Delivery is at-least-once and ordering is not guaranteed. Deduplicate on the event
id, and prefer the state in data over inferring it from event sequence.
Debugging. POST /webhooks/endpoints/{id}/test sends a real signed event with obviously fake data and reports what your server answered. GET /webhooks/deliveries is the delivery log: status, response code, attempt count and next retry.
No public endpoint? Every event is also readable from GET /events. Poll it with after set to the last event id you handled. Events are retained for 30 days.
Event types
activation.created — An activation was purchased and its number is ready to receive SMS.activation.message.received — An SMS arrived on an activation. The extracted verification code is in data.messages[0].code when one could be parsed.activation.completed — An activation was marked finished and will receive no further messages.activation.cancelled — An activation was cancelled before use and the balance was refunded.activation.expired — An activation reached its expiry window without receiving an SMS.activation.refunded — The balance for an activation was returned to the account.rental.created — A long-term rental was ordered. It may still be provisioning.rental.activated — A rental finished provisioning and is now live.rental.message.received — An SMS arrived on a rental number.rental.extended — A rental was extended and its expiry moved forward.rental.expiring — A rental expires within 24 hours.rental.expired — A rental reached the end of its period and stopped receiving messages.rental.cancelled — A rental was cancelled.balance.updated — The account balance changed.