Every webhook includes:
X-PERCENTS-SIGNATURE: t=<epoch-milliseconds>,s=<hex-hmac-sha256>Percents computes HMAC-SHA256 over <timestamp>.<JSON request body> using the issuer's sign_ webhook signing token.
- Capture the JSON request body exactly as received.
- Parse
tandsfrom the signature header. - Reject stale timestamps according to your replay policy.
- Compute HMAC-SHA256 with the signing token over
<t>.<raw body>. - Compare signatures with a constant-time comparison.
- Deduplicate the verified
webhookIdbefore applying side effects.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyPercentsSignature(input: {
header: string;
rawBody: string;
signingToken: string;
}): boolean {
const match = /^t=(\d+),s=([a-f0-9]+)$/i.exec(input.header);
if (!match) {
return false;
}
const [, timestamp, receivedHex] = match;
const expectedHex = createHmac('sha256', input.signingToken)
.update(`${timestamp}.${input.rawBody}`)
.digest('hex');
const expected = Buffer.from(expectedHex, 'hex');
const received = Buffer.from(receivedHex, 'hex');
return expected.length === received.length && timingSafeEqual(expected, received);
}Store the sign_ token separately from the api_ API secret. Contact Percents to rotate it if exposed.