Choose a language

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
  1. Events
  2. Create a webhook
  3. What a delivery looks like
  4. Verify the signature
  5. Answer quickly
  6. Retries
  7. Automatic switch-off
  8. Test and redeliver

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

HTTP
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.

PHP
[$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);
Python
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", ""))
JavaScript
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 Gone switches 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?

Still need help?

Write to us. We answer every message, usually within a few hours.

Or email us at support@smsgrab.com