Verify webhook signatures to ensure incoming events are authentic.
Prerequisites
You will need:
A webhook subscription secret from Subscriptions
Access to the raw, unmodified request body in your webhook handler
The X-Hook0-Signature request header
Verify signatures
Verify webhook signatures to ensure that requests are authentic. This protects your application from forged webhooks and potential security threats.
How signature verification works
When you create a webhook subscription, the response includes a secret. This secret is used to verify that incoming webhooks are authentic.
Each webhook request includes an X-Hook0-Signature header containing:
t field - the timestamp
h field - list of headers included in the signature
v1 field - the signature
Verification implementation
Here’s an example of how to verify webhook signatures:
TypeScript
Python
Go
Ruby
PHP
Java
import crypto from "crypto" ;
type HeadersMap = Record < string , string >;
function verifyWebhookSignature (
payload : string ,
signatureHeader : string ,
secret : string ,
headers : HeadersMap ,
maxAgeMinutes = 5
) : boolean {
try {
const elements = signatureHeader . split ( "," );
const timestamp = elements . find (( e ) => e . startsWith ( "t=" ))?. split ( "=" )[ 1 ] ?? "" ;
const headerNames = elements . find (( e ) => e . startsWith ( "h=" ))?. split ( "=" )[ 1 ] ?? "" ;
const providedSignature = elements . find (( e ) => e . startsWith ( "v1=" ))?. split ( "=" )[ 1 ] ?? "" ;
const headerNameList = headerNames . split ( " " );
const headerValues = headerNameList . map (( name ) => headers [ name ] ?? "" ). join ( "." );
const signedPayload = ` ${ timestamp } . ${ headerNames } . ${ headerValues } . ${ payload } ` ;
const expectedSignature = crypto
. createHmac ( "sha256" , secret )
. update ( signedPayload , "utf8" )
. digest ( "hex" );
const signaturesMatch = crypto . timingSafeEqual (
Buffer . from ( expectedSignature , "hex" ),
Buffer . from ( providedSignature , "hex" )
);
const webhookTime = parseInt ( timestamp , 10 ) * 1000 ;
const currentTime = Date . now ();
const ageMinutes = ( currentTime - webhookTime ) / ( 1000 * 60 );
if ( ageMinutes > maxAgeMinutes ) {
console . error (
`Webhook timestamp exceeds maximum age: ${ ageMinutes . toFixed ( 1 ) } minutes > ${ maxAgeMinutes } minutes`
);
return false ;
}
return signaturesMatch ;
} catch ( error ) {
console . error ( "Webhook verification error:" , error );
return false ;
}
}
And in your application:
TypeScript
Python
Go
Ruby
PHP
Java
import express from "express" ;
const app = express ();
// Important: Get raw body for signature verification
app . use ( express . raw ({ type: "application/json" }));
app . post ( "/webhook" , ( req , res ) => {
const payload = req . body . toString ();
const signature = req . headers [ "x-hook0-signature" ];
const secret = process . env . WEBHOOK_SECRET ?? "" ;
if ( verifyWebhookSignature ( payload , String ( signature || "" ), secret , req . headers as Record < string , string >)) {
const event = JSON . parse ( payload );
// Handle your transfer event here
res . status ( 200 ). send ( "OK" );
return ;
}
res . status ( 400 ). send ( "Invalid signature" );
});