Zavu signs all webhook requests with an HMAC-SHA256 signature. You should verify this signature to ensure the request is authentic and hasn’t been tampered with.
Every webhook request includes an X-Zavu-Signature header:
X-Zavu-Signature: t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The header contains:
Part Description tUnix timestamp (seconds) when the signature was generated v1HMAC-SHA256 signature of the payload
Verifying Signatures
Parse the X-Zavu-Signature header to get the timestamp and signature:
function parseSignature ( header ) {
const parts = header . split ( ',' );
const timestamp = parts . find ( p => p . startsWith ( 't=' )). slice ( 2 );
const signature = parts . find ( p => p . startsWith ( 'v1=' )). slice ( 3 );
return { timestamp: parseInt ( timestamp ), signature };
}
Step 2: Prepare the Signed Payload
Concatenate the timestamp and the raw request body:
const signedPayload = ` ${ timestamp } . ${ rawBody } ` ;
Step 3: Compute the Expected Signature
Use HMAC-SHA256 with your webhook secret:
const crypto = require ( 'crypto' );
function computeSignature ( secret , payload ) {
return crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
}
Step 4: Compare Signatures
Use a constant-time comparison to prevent timing attacks:
const crypto = require ( 'crypto' );
function verifySignature ( expected , actual ) {
return crypto . timingSafeEqual (
Buffer . from ( expected ),
Buffer . from ( actual )
);
}
Complete Examples
TypeScript (Express)
Python (Flask)
Go
import crypto from "crypto" ;
import express from "express" ;
const app = express ();
// Use raw body for signature verification
app . use ( "/webhooks/zavu" , express . raw ({ type: "application/json" }));
function verifyZavuSignature ( req , secret ) {
const signatureHeader = req . headers [ "x-zavu-signature" ];
if ( ! signatureHeader ) {
return false ;
}
// Parse the signature header
const parts = signatureHeader . split ( "," );
const timestampPart = parts . find ( p => p . startsWith ( "t=" ));
const signaturePart = parts . find ( p => p . startsWith ( "v1=" ));
if ( ! timestampPart || ! signaturePart ) {
return false ;
}
const timestamp = parseInt ( timestampPart . slice ( 2 ));
const signature = signaturePart . slice ( 3 );
// Check timestamp (reject if older than 5 minutes)
const now = Math . floor ( Date . now () / 1000 );
if ( now - timestamp > 300 ) {
console . log ( "Webhook timestamp too old" );
return false ;
}
// Compute expected signature
const rawBody = req . body . toString ();
const signedPayload = ` ${ timestamp } . ${ rawBody } ` ;
const expectedSignature = crypto
. createHmac ( "sha256" , secret )
. update ( signedPayload )
. digest ( "hex" );
// Constant-time comparison
try {
return crypto . timingSafeEqual (
Buffer . from ( expectedSignature ),
Buffer . from ( signature )
);
} catch {
return false ;
}
}
app . post ( "/webhooks/zavu" , ( req , res ) => {
const isValid = verifyZavuSignature ( req , process . env . ZAVU_WEBHOOK_SECRET );
if ( ! isValid ) {
console . log ( "Invalid webhook signature" );
return res . status ( 401 ). send ( "Invalid signature" );
}
// Parse the body and process
const event = JSON . parse ( req . body . toString ());
console . log ( "Verified webhook:" , event . type );
res . status ( 200 ). send ( "OK" );
});
app . listen ( 3000 );
Timestamp Validation
Always validate the timestamp to prevent replay attacks:
const MAX_AGE_SECONDS = 300 ; // 5 minutes
function isTimestampValid ( timestamp ) {
const now = Math . floor ( Date . now () / 1000 );
return ( now - timestamp ) <= MAX_AGE_SECONDS ;
}
Never skip signature verification in production. An attacker could send fake webhook events to your endpoint.
Troubleshooting
Signature Mismatch
If signature verification fails:
Check your secret - Ensure you’re using the correct webhook secret from the sender’s webhook configuration
Use raw body - The signature is computed on the raw request body, not parsed JSON
Check encoding - Ensure the body is UTF-8 encoded
Verify timestamp format - The timestamp in the signature is in seconds, not milliseconds
Testing Locally
For local development, you can temporarily disable signature verification or use a tool like ngrok to expose your local server.
// Development only - never use in production!
const SKIP_VERIFICATION = process . env . NODE_ENV === 'development' ;
app . post ( '/webhooks/zavu' , ( req , res ) => {
if ( ! SKIP_VERIFICATION && ! verifyZavuSignature ( req , secret )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// ...
});
Next Steps