Use these handlers as your starting point. Each example verifies the X-Webhook-Signature header against the raw request body using your endpoint secret.
Always verify against the raw request body before parsing JSON. Any body transformation can invalidate the signature.
Reusable verify function
verify.ts (Node.js)
verify.py (Python)
import crypto from "crypto" ;
export function verifyWebhook (
rawBody : string | Buffer ,
timestampHeader : string ,
signatureHeader : string ,
secret : string ,
toleranceSeconds = 300 ,
) {
const timestamp = Number ( timestampHeader );
if (! timestamp ) throw new Error ( "Missing or invalid webhook timestamp" );
const age = Math . floor ( Date . now () / 1000 ) - timestamp ;
if ( age > toleranceSeconds ) {
throw new Error ( "Timestamp outside tolerance" );
}
const body = typeof rawBody === "string" ? rawBody : rawBody . toString ( "utf8" );
const expected = crypto
. createHmac ( "sha256" , secret )
. update ( ` ${ timestamp } . ${ body } ` , "utf8" )
. digest ( "hex" );
const received = String ( signatureHeader ). replace ( / ^ sha256=/ i , "" ). trim ();
const a = Buffer . from ( expected , "utf8" );
const b = Buffer . from ( received , "utf8" );
if ( a . length !== b . length || ! crypto . timingSafeEqual ( a , b )) {
throw new Error ( "Invalid signature" );
}
return JSON . parse ( body );
}
Express (Node.js)
import express from "express" ;
import { verifyWebhook } from "./verify" ;
const app = express ();
app . post (
"/webhooks/zquence" ,
express . raw ({ type: "application/json" }),
async ( req , res ) => {
let event ;
try {
event = verifyWebhook (
req . body ,
String ( req . headers [ "x-webhook-timestamp" ] || "" ),
String ( req . headers [ "x-webhook-signature" ] || "" ),
process . env . ZQUENCE_WEBHOOK_SECRET !,
);
} catch {
return res . status ( 400 ). send ( "Invalid webhook" );
}
await enqueue ( event );
return res . sendStatus ( 200 );
},
);
Next.js (App Router)
import { NextResponse } from "next/server" ;
import { verifyWebhook } from "@/lib/verify" ;
export const runtime = "nodejs" ;
export const dynamic = "force-dynamic" ;
export async function POST ( req : Request ) {
const body = await req . text ();
const timestamp = req . headers . get ( "x-webhook-timestamp" ) || "" ;
const signature = req . headers . get ( "x-webhook-signature" ) || "" ;
let event ;
try {
event = verifyWebhook ( body , timestamp , signature , process . env . ZQUENCE_WEBHOOK_SECRET !);
} catch {
return new NextResponse ( "Invalid webhook" , { status: 400 });
}
await enqueue ( event );
return NextResponse . json ({ received: true });
}
Flask (Python)
from flask import Flask, request
from verify import verify_webhook
import os
app = Flask( __name__ )
@app.post ( "/webhooks/zquence" )
def handle ():
try :
event = verify_webhook(
request.data,
request.headers.get( "X-Webhook-Timestamp" , "" ),
request.headers.get( "X-Webhook-Signature" , "" ),
os.environ[ "ZQUENCE_WEBHOOK_SECRET" ],
)
except ValueError :
return "Invalid webhook" , 400
enqueue(event)
return "" , 200
Response codes
Current delivery behavior is based only on HTTP success vs failure:
2xx → delivery succeeds and stops retrying.
Non-2xx → delivery is marked failed and retried if attempts remain.
Timeout after 10 seconds → delivery is marked failed and retried if attempts remain.
Keep your handler small: verify, enqueue, return 200. Do not do slow business logic inline.