VUZDevelopers
Webhooks

Signature verification

The real HMAC-SHA256 scheme VUZ uses — verified against the delivery code, not assumed.

This is not the Stripe-style t=,v1= scheme you may have seen described elsewhere. VUZ's actual implementation signs the raw payload only — the timestamp header is separate and unsigned. Use the exact recipe below.

The headers VUZ sends

Content-Type: application/json
User-Agent: VUZ-Webhook/1.0
X-Vuz-Signature: 5f3d8a1c9b...   (hex-encoded HMAC-SHA256)
X-Vuz-Timestamp: 1751371260      (unix seconds — NOT part of the signature)
X-Vuz-Delivery-Id: 9c2f6b2e-2e1e-4b7e-9f1e-1e2e3e4e5e6e
X-Vuz-Event: document.finalized

How the signature is computed

signature = hex( HMAC_SHA256( endpoint_secret, JSON.stringify(payload) ) )

endpoint_secret is the random secret generated when you created the webhook endpoint (POST /api/v1/webhooks). The HMAC input is only the JSON-serialized payload body — X-Vuz-Timestamp is not mixed into the signature, so you don't need it to verify (it's informational / useful for your own replay-window policy, not part of the cryptographic check).

Verify against the raw request body bytes, not a value you re-serialize yourself. JSON.stringify(JSON.parse(body)) can reorder keys or change number/whitespace formatting and silently break signature comparison. Capture the raw body before your framework parses it as JSON.

Verify it — Node.js

import crypto from 'crypto';
import express from 'express';

const app = express();

// IMPORTANT: capture the raw body BEFORE JSON parsing
app.post(
  '/webhooks/vuz',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-vuz-signature'];
    const rawBody = req.body; // Buffer, exact bytes VUZ sent

    const expected = crypto
      .createHmac('sha256', process.env.VUZ_WEBHOOK_SECRET)
      .update(rawBody)
      .digest('hex');

    const valid =
      signature &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

    if (!valid) {
      return res.status(401).send('invalid signature');
    }

    const event = JSON.parse(rawBody.toString('utf8'));
    console.log('Verified event:', event.event ?? req.headers['x-vuz-event']);
    res.status(200).send('ok');
  },
);

Verify it — PHP

<?php
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_VUZ_SIGNATURE'] ?? '';
$secret = getenv('VUZ_WEBHOOK_SECRET');

$expected = hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('invalid signature');
}

$event = json_decode($rawBody, true);
http_response_code(200);
echo 'ok';

Verify it — Python

import hashlib
import hmac
import os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['VUZ_WEBHOOK_SECRET']

@app.post('/webhooks/vuz')
def handle_webhook():
    raw_body = request.get_data()  # exact bytes VUZ sent
    signature = request.headers.get('X-Vuz-Signature', '')

    expected = hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(401, 'invalid signature')

    event = request.get_json()
    print('Verified event:', event.get('event'))
    return 'ok', 200

Test against a real signature

POST /api/v1/webhooks/{id}/test fires a real, correctly signed delivery at your endpoint — use it to confirm your verification code against a live signature before going to production, rather than hand-computing a fixture.

On this page