Transcom Group Veritus

Verifying webhook signatures

Every webhook delivery carries an HMAC-SHA256 signature so you can verify it really came from us and the body wasn't tampered with in transit. Compute the same HMAC on your end and compare in constant time.

The headers

  • X-Veritus-Signature: sha256=<hex> — HMAC of the raw request body
  • X-Veritus-Event: check.blocked — event type
  • X-Veritus-Delivery: <uuid> — unique delivery ID (use for idempotency)
  • X-Veritus-Timestamp: 1748094182 — when we sent it (unix seconds)

Python (Flask)

import hmac, hashlib
from flask import request, abort

SECRET = b"your-webhook-signing-secret"

@app.post("/veritus/webhook")
def veritus_webhook():
    body = request.get_data()  # raw bytes — DO NOT use .json or .form
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    given = request.headers.get("X-Veritus-Signature", "")
    if not hmac.compare_digest(expected, given):
        abort(401)
    payload = request.get_json()
    # ... handle payload
    return "", 200

Node.js (Express)

const crypto = require('crypto');
const SECRET = 'your-webhook-signing-secret';

// IMPORTANT: capture raw body BEFORE express.json() parses it
app.use('/veritus/webhook', express.raw({ type: 'application/json' }));

app.post('/veritus/webhook', (req, res) => {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const given = req.headers['x-veritus-signature'] || '';
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
    return res.status(401).end();
  }
  const payload = JSON.parse(req.body.toString());
  // ... handle payload
  res.sendStatus(200);
});

PHP

<?php
$secret = 'your-webhook-signing-secret';
$body   = file_get_contents('php://input');  // raw body

$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$given    = $_SERVER['HTTP_X_VERITUS_SIGNATURE'] ?? '';

if (!hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($body, true);
// ... handle payload
http_response_code(200);

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
)

var secret = []byte("your-webhook-signing-secret")

func handler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    mac := hmac.New(sha256.New, secret)
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    given := r.Header.Get("X-Veritus-Signature")
    if !hmac.Equal([]byte(expected), []byte(given)) {
        w.WriteHeader(401)
        return
    }
    // parse and handle body
    w.WriteHeader(200)
}

Common mistakes

  • Comparing after JSON parsing. Sign over the raw bytes you received, not a re-serialised version.
  • Using == for the comparison. Use a constant-time function (hmac.compare_digest, timingSafeEqual) to avoid timing attacks.
  • Storing the secret in code or git. Use environment variables.
  • Forgetting the sha256= prefix. Our header always includes it.
Found a typo or have a suggestion? Let us know.