Verifying Callback Signatures
To verify that a callback genuinely came from ReviewerZero and is not a replay of an earlier request:
- Read the
X-ReviewerZero-Timestampheader. Reject the request if it is more than 5 minutes (300 seconds) before or after your current time. - Build the signed string: the timestamp, a period, then the raw request body exactly as received (
<timestamp>.<raw body>). - Compute the HMAC-SHA256 of that string using your API key as the secret, and compare it to the
X-ReviewerZero-Signature-V2header with a constant-time comparison.
Each delivery attempt carries its own timestamp and signature, so a retry is never rejected as stale.
Important: The signature is computed over a SHA-256 hash of your API key, not the raw API key itself. ReviewerZero does not store your raw API key.
Python Example
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_signature(raw_body: bytes, timestamp_header: str,
signature_header: str, api_key: str) -> bool:
"""Verify the X-ReviewerZero-Signature-V2 and X-ReviewerZero-Timestamp headers."""
try:
timestamp = int(timestamp_header)
except (TypeError, ValueError):
return False
if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()
expected = hmac.new(
api_key_hash.encode(),
timestamp_header.encode() + b"." + raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)Node.js Example
const crypto = require('crypto');
const TOLERANCE_SECONDS = 300;
function verifySignature(rawBody, timestampHeader, signatureHeader, apiKey) {
const timestamp = Number(timestampHeader);
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const apiKeyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const expected = crypto.createHmac('sha256', apiKeyHash)
.update(`${timestampHeader}.`)
.update(rawBody)
.digest('hex');
const received = Buffer.from(signatureHeader || '');
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), received);
}Legacy Signature
X-ReviewerZero-Signature is still sent on every callback for compatibility. It is the HMAC-SHA256 of the raw request body alone, with the same secret. Because it does not cover the timestamp, it cannot detect a replayed request; new integrations should verify X-ReviewerZero-Signature-V2.