New to VisionScaper?
Start here
Help Center › API keys & connections › Verifying an alert's HMAC-SHA256 signature
Verifying an alert’s HMAC-SHA256 signature
Check that an alert really came from VisionScaper using the X-VisionScaper-Signature header and your secret, with PHP, Node and Python examples.
On this page
Anyone can send a POST to your address. The signature proves it came from VisionScaper.
How it is built
Each alert carries this header:
X-VisionScaper-Signature: t=1767621600,v1=9f2c...
tis a Unix time in seconds.v1is the lowercase hex of HMAC-SHA256 over the stringt + "." + raw body, keyed with the alert’s secret (whsec_...).
Steps to verify
- Read the raw request body. Do not parse and re-encode it first.
- Read the header. Split it on the comma to get
tandv1. - Compute
HMAC-SHA256(secret, t + "." + body)as hex. - Compare it to
v1in constant time. - Reject if
tis more than 5 minutes (300 seconds) from your clock. - Only then trust and parse the JSON.
PHP
$raw = file_get_contents('php://input');
[$t, $v] = array_map(fn($p) => explode('=', $p, 2)[1],
explode(',', $_SERVER['HTTP_X_VISIONSCAPER_SIGNATURE']));
$ok = hash_equals(hash_hmac('sha256', $t . '.' . $raw, $secret), $v)
&& abs(time() - (int)$t) < 300;
if (!$ok) { http_response_code(400); exit; }
Node.js
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const mac = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody).digest('hex');
const same = mac.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1));
return same && Math.abs(Date.now()/1000 - Number(parts.t)) < 300;
}
Python
import hmac, hashlib, time
def verify(raw: bytes, header: str, secret: str) -> bool:
d = dict(p.split('=', 1) for p in header.split(','))
mac = hmac.new(secret.encode(), (d['t'] + '.').encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, d['v1']) and abs(time.time() - int(d['t'])) < 300
(The Node and Python examples follow the same rule as the PHP one in the product notes. They were not run here.)
Tips
- Keep the secret out of source control.
- Use the delivery id (
X-VisionScaper-Delivery) and event id to ignore repeats. - A new secret can be made from the alert. The old one stops working at once. Update your receiver first, then rotate.
- A fresh timestamp is sent on each retry, so the 5-minute rule works for retries.
If something goes wrong
- Signatures never match: you probably hashed a re-encoded body. Use the raw bytes.
- Only some fail: check clock drift on your server.
- Header missing: the request was not from VisionScaper.