Webhooks: Verify the signature (HMAC)
Every webhook (v2) request we send to your endpoint is signed with the
secret of that endpoint (the secret is returned once when you create the
endpoint via Create endpoint). Verifying the signature guarantees that
the request really came from Smstools and that the payload was not altered.
Each request carries two extra headers:
X-Smstools-Timestamp: 1786606200 X-Smstools-Signature: t=1786606200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
How to verify:
- Read the timestamp
tand signaturev1from theX-Smstools-Signatureheader. - Build the signed payload:
{t}+ "." + the raw request body, exactly as received. Do not decode and re-encode the JSON: any change to the byte sequence changes the signature. For GET webhooks, use the url-encoded query string of the request instead of the body. - Compute the HMAC-SHA256 of the signed payload with your endpoint secret as key. The result is a lowercase hex string.
- Compare it with
v1using a constant-time comparison, and reject requests whose timestamp is too old (e.g. more than 5 minutes) to block replays.
Example verification (PHP):
<?php
$secret = "ccc14fb1-bf8c-4ebf-882c-caccd4c95a2c"; // secret of your endpoint
$rawBody = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_SMSTOOLS_SIGNATURE"]; // t={timestamp},v1={signature}
parse_str(str_replace(",", "&", $header), $sig); // ["t" => ..., "v1" => ...]
$expected = hash_hmac("sha256", $sig["t"] . "." . $rawBody, $secret);
$valid = hash_equals($expected, $sig["v1"])
&& abs(time() - (int) $sig["t"]) < 300; // reject replays (5 min)
if (!$valid) {
http_response_code(400);
exit;
}
?>
Endpoints created without webhooks v2 (or without a secret) are not signed: the headers are simply absent. Treat a missing signature on a v2 endpoint as invalid.