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.

Signature header

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

  1. Read the raw request body (JSON string, unchanged).
  2. Parse t from the signature header.
  3. Build the signed payload: {t}.{raw_body}
  4. Compute HMAC-SHA256(signed_payload, webhook_secret) as lowercase hex.
  5. 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.