Every webhook carries X-UPI-Timestamp and X-UPI-Signature: sha256=…, where the signature is HMAC-SHA256 of timestamp + "." + raw body with your signing secret.
PHP
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_UPI_TIMESTAMP'] ?? '';
$ok = hash_equals('sha256='.hash_hmac('sha256', $ts.'.'.$body, $secret), $_SERVER['HTTP_X_UPI_SIGNATURE'] ?? '');
Node.js
const sig = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.get('X-UPI-Signature') || ''));
Python
expected = 'sha256=' + hmac.new(secret.encode(), (ts + '.').encode() + raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers.get('X-UPI-Signature', ''))
Two more rules
- Use the raw body, before any JSON parsing.
- Reject timestamps older than about five minutes to stop replays.
0 comments