Verifying Callback Signatures

To verify that a callback genuinely came from ReviewerZero and is not a replay of an earlier request:

  1. Read the X-ReviewerZero-Timestamp header. Reject the request if it is more than 5 minutes (300 seconds) before or after your current time.
  2. Build the signed string: the timestamp, a period, then the raw request body exactly as received (<timestamp>.<raw body>).
  3. Compute the HMAC-SHA256 of that string using your API key as the secret, and compare it to the X-ReviewerZero-Signature-V2 header 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.