X-Telehealth-Signature header in lowercase hexadecimal (no prefix). You must compute HMAC-SHA256 of the raw body (UTF-8) with your secret and compare in lowercase hex.
- PHP
- Python
- Node.js
- Java
Copy
<?php
function verifyWebhookSignature(string $rawBody, string $signatureHeader, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret, false); // false = lowercase hex
return hash_equals($expected, $signatureHeader);
}
// In your webhook endpoint:
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TELEHEALTH_SIGNATURE'] ?? '';
$secret = 'YOUR_WEBHOOK_SECRET';
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
// Process $payload according to $payload['event'] or X-Telehealth-Event header
Copy
import hmac
import hashlib
def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# In your webhook endpoint (e.g. Flask):
# raw_body = request.get_data()
# signature = request.headers.get("X-Telehealth-Signature", "")
# if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
# return "", 401
# payload = request.get_json(force=True, silent=True)
Copy
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signatureHeader, 'hex')
);
}
// In your endpoint (e.g. Express): use express.raw() for this route to get unparsed body
// const rawBody = req.body; // Buffer
// const signature = req.headers['x-telehealth-signature'] || '';
// if (!verifyWebhookSignature(rawBody.toString('utf8'), signature, process.env.WEBHOOK_SECRET)) {
// return res.status(401).send('Invalid signature');
// }
// const payload = JSON.parse(rawBody.toString('utf8'));
Copy
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
public static boolean verifyWebhookSignature(String rawBody, String signatureHeader, String secret)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(hash).toLowerCase();
return java.security.MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signatureHeader.getBytes(StandardCharsets.UTF_8)
);
}
// In your controller: read raw body (do not parse to JSON first), then verify, then parse.
