Skip to content
Last updated

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.

Verification steps

  1. Capture the JSON request body exactly as received.
  2. Parse t and s from the signature header.
  3. Reject stale timestamps according to your replay policy.
  4. Compute HMAC-SHA256 with the signing token over <t>.<raw body>.
  5. Compare signatures with a constant-time comparison.
  6. Deduplicate the verified webhookId before 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.