Verify webhook signatures
Each webhook has a signing secret (whsec_…) shown once when you create or rotate it. Use this secret to verify that deliveries were sent by Tracksales.
X-Tracksales-Signature contains a timestamp and HMAC:
t=1720425600,v1=8f2c9b1e4a7d3f6e0c5b8a2d1e9f4c7b6a3d8e1f0c2b5a4d7e9f1c3b6a8d2e5
t — Unix timestamp when the signature was generated
v1 — HMAC-SHA256 hex digest
Computing the expected signature
Read the raw request body (JSON string, unchanged).
Parse t from the signature header.
Build the signed payload: {t}.{raw_body}
Compute HMAC-SHA256(signed_payload, webhook_secret) as lowercase hex.
Compare to v1 using a constant-time comparison.
PHP example
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_TRACKSALES_SIGNATURE'] ?? '';
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $header, $matches);
$timestamp = (int) ($matches[1] ?? 0);
$received = $matches[2] ?? '';
$signed = $timestamp.'.'.$payload;
$expected = hash_hmac('sha256', $signed, $webhookSecret);
if (! hash_equals($expected, $received)) {
http_response_code(400);
exit('Invalid signature');
}
Node.js example
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const match = /t=(\d+),v1=([a-f0-9]+)/.exec(signatureHeader || '');
if (!match) return false;
const timestamp = match[1];
const received = match[2];
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex')
);
}
Replay protection
Reject signatures older than 5 minutes to limit replay attacks:
if (abs(time() - $timestamp) > 300) {
http_response_code(400);
exit('Timestamp too old');
}
Secret rotation
Rotate the signing secret from Developer API settings if it is compromised. Update your verifier immediately — old signatures will no longer validate.