Verifying Signatures

Every webhook delivery carries an X-RYNT-Signature header so you can confirm it genuinely came from RYNT and wasn't tampered with or replayed.

The header

X-RYNT-Signature: t=1786970433,v1=cfd2cf2e81e8660d4969b69ccdb2bb7827a8a6ce...
  • t — the Unix timestamp when the event was signed.
  • v1HMAC_SHA256(secret, "<t>.<rawBody>") in hex, where secret is the value returned when you registered the webhook.

Verify (Node.js)

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.` + rawBody)
    .digest('hex');

  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 min
  const match = crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(parts.v1)
  );
  return fresh && match;
}

Verify (PHP)

function verify(string $rawBody, string $header, string $secret): bool {
  parse_str(str_replace(',', '&', $header), $p); // t=…&v1=…
  $expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
  $fresh = abs(time() - (int)$p['t']) < 300;
  return $fresh && hash_equals($expected, $p['v1']);
}
🚧

Use the raw body

Compute the HMAC over the exact bytes you received, before any JSON parse/re-serialize. Re-stringifying can change spacing and break the signature. Reject anything that doesn't match or whose timestamp is older than a few minutes.


Did this page help you?