Webhooks
Receive activation events on your server in real time, verify the SmsGrab-Signature header, and understand retries, test deliveries and automatic switch-off.
On this page
Webhooks send events to your server the moment they happen, so you do not have to poll. A webhook belongs to one API key. It receives the events of numbers bought with that key, plus low-balance alerts for your account.
Events
| Event | When |
|---|---|
activation.created |
A number was bought |
activation.code_received |
Each new SMS, with every message so far |
activation.completed |
Finished by you, or when the time ran out after a code |
activation.cancelled |
Cancelled before a code, refunded |
activation.expired |
No code in time, refunded |
activation.refunded |
Any refund of the charge |
balance.low |
Your balance fell below the threshold you set |
webhook.test |
A test delivery you started |
Create a webhook
In Account > Developers, open a key and add a webhook with its URL, the events you want and an optional description. Each key can have up to 3 webhooks.
The URL must:
- start with
https://; - use a host name, not an IP address;
- lead to a public address. Private networks, localhost and similar ranges are refused.
You receive a signing secret that starts with whsec_. Like an API key, it is shown only once. You can roll it later, which also asks for your password.
What a delivery looks like
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: SmsGrab-Webhooks/1.0
SmsGrab-Event: activation.code_received
SmsGrab-Delivery: 9f7d6c1e-2b44-4c1a-8f0e-6a3d2c1b0e9f
SmsGrab-Signature: t=1790503200,v1=3b9a0c…
{ "id": "evt_4e1f…", "type": "activation.code_received", "created_at": "2026-09-27T10:06:12.004Z",
"api_version": "2026-09-27", "data": { "activation": { "id": "5c1d0c2e-…", "status": "CODE_RECEIVED" } } }
Verify the signature
SmsGrab-Signature holds a timestamp t and v1, the HMAC-SHA256 of t, a dot and the raw request body, keyed with your secret. Reject deliveries whose timestamp is more than 300 seconds away from your clock, and compare the signatures in constant time.
[$t, $v1] = (function (string $h): array {
parse_str(str_replace(',', '&', $h), $p);
return [(int) ($p['t'] ?? 0), (string) ($p['v1'] ?? '')];
})($_SERVER['HTTP_SMSGRAB_SIGNATURE'] ?? '');
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $t.'.'.$body, getenv('SMSGRAB_WEBHOOK_SECRET'));
$ok = abs(time() - $t) <= 300 && hash_equals($expected, $v1);
import hmac, hashlib, time
def verify(header: str, body: bytes, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return abs(time.time() - t) <= 300 and hmac.compare_digest(expected, parts.get("v1", ""))
import crypto from 'node:crypto';
export function verify(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t ?? 0);
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
const given = Buffer.from(parts.v1 ?? '', 'utf8');
return Math.abs(Date.now() / 1000 - t) <= 300 && given.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), given);
}
Always check the raw body exactly as you received it, before parsing the JSON.
Answer quickly
Answer with any 2xx status within 10 seconds and do slow work afterwards, for example in a queue. Redirects are not followed.
Retries
A failed delivery is retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours and 24 hours. A retry repeats the same event id, so remember the ids you have processed and skip duplicates. Events for one activation arrive in order.
Automatic switch-off
- Answering
410 Goneswitches the webhook off at once. - After 50 failed attempts in a row over at least 24 hours, the webhook is switched off and you get a notification. Fix your endpoint, then switch the webhook back on.
- Revoking a key switches its webhooks off.
Test and redeliver
Send a webhook.test event from the webhook's page to see the status code, the response time and any error straight away. The delivery list keeps every delivery for 30 days, and you can send any of them again.
Was this article helpful?
Thanks! Glad it helped.