Macropay uses the Standard Webhooks specification to sign all webhook deliveries. You should verify the signature of every webhook you receive to ensure it was sent by Macropay and has not been tampered with.
How It Works
Each webhook delivery includes three headers:
Header Description webhook-idA unique identifier for this webhook event webhook-timestampUnix timestamp (seconds) when the event was sent webhook-signatureOne or more Base64-encoded HMAC-SHA256 signatures, comma-separated
The signature is computed over the string {webhook-id}.{webhook-timestamp}.{body} using your endpoint’s signing secret as the HMAC key.
Getting Your Signing Secret
Your webhook signing secret is generated when you create a webhook endpoint. You can find it in:
The Macropay dashboard under Settings > Webhooks > select your endpoint
The API response when creating a webhook endpoint
The secret starts with whsec_ followed by a Base64-encoded key.
Verification Examples
Using Standard Webhooks Libraries (Recommended)
The easiest approach is to use the official Standard Webhooks libraries, which handle all the details for you.
import { Webhook } from "standardwebhooks" ;
const WEBHOOK_SECRET = process . env . MACROPAY_WEBHOOK_SECRET ! ;
export async function handleWebhook ( request : Request ) : Promise < Response > {
const body = await request . text ();
const headers = {
"webhook-id" : request . headers . get ( "webhook-id" ) ! ,
"webhook-timestamp" : request . headers . get ( "webhook-timestamp" ) ! ,
"webhook-signature" : request . headers . get ( "webhook-signature" ) ! ,
};
const wh = new Webhook ( WEBHOOK_SECRET );
try {
const payload = wh . verify ( body , headers );
// payload is the verified and parsed webhook event
console . log ( "Verified event:" , payload . type );
// Handle the event...
return new Response ( "OK" , { status: 200 });
} catch ( err ) {
console . error ( "Webhook verification failed:" , err );
return new Response ( "Invalid signature" , { status: 401 });
}
}
Install the libraries:
npm install standardwebhooks
Manual Verification
If you prefer to verify signatures manually without a library:
import { createHmac , timingSafeEqual } from "crypto" ;
function verifyWebhook (
body : string ,
headers : Record < string , string >,
secret : string
) : boolean {
const msgId = headers [ "webhook-id" ];
const timestamp = headers [ "webhook-timestamp" ];
const signatures = headers [ "webhook-signature" ];
// 1. Check timestamp tolerance (5 minutes)
const now = Math . floor ( Date . now () / 1000 );
const ts = parseInt ( timestamp , 10 );
if ( Math . abs ( now - ts ) > 300 ) {
throw new Error ( "Webhook timestamp too old or too new" );
}
// 2. Compute expected signature
const signedContent = ` ${ msgId } . ${ timestamp } . ${ body } ` ;
// Remove the "whsec_" prefix and decode the Base64 secret
const secretBytes = Buffer . from ( secret . replace ( "whsec_" , "" ), "base64" );
const expectedSignature = createHmac ( "sha256" , secretBytes )
. update ( signedContent )
. digest ( "base64" );
// 3. Compare against provided signatures (there may be multiple)
const providedSignatures = signatures . split ( " " );
for ( const sig of providedSignatures ) {
// Each signature is prefixed with version, e.g. "v1,<base64>"
const [, sigValue ] = sig . split ( "," );
if (
timingSafeEqual (
Buffer . from ( expectedSignature ),
Buffer . from ( sigValue )
)
) {
return true ;
}
}
return false ;
}
Security Best Practices
Always verify signatures in production. Without verification, an attacker
could send fake webhook events to your endpoint and trigger unauthorized
actions in your system.
Use timing-safe comparison : Always use timingSafeEqual (Node.js) or hmac.compare_digest (Python) to compare signatures. Regular string comparison is vulnerable to timing attacks.
Check the timestamp : Reject events with timestamps older than 5 minutes to prevent replay attacks.
Use HTTPS : Always configure your webhook endpoint with an HTTPS URL.
Respond quickly : Return a 2xx status code within 30 seconds. Process the event asynchronously if needed.
Handle retries : Macropay retries failed deliveries with exponential backoff. Make your handler idempotent to safely handle duplicate events. See Webhook Delivery for retry details.
Framework-Specific Helpers
If you are using one of our framework adapters, webhook verification is built in:
Next.js Built-in webhook handler with automatic verification
Express Middleware for Express webhook routes
Fastify Plugin for Fastify webhook routes
Hono Middleware for Hono webhook routes