import express from "express";import Align from "@tolbel/align";const app = express();const align = new Align({ apiKey: process.env.ALIGN_API_KEY!, environment: "production",});// IMPORTANT: Use express.raw() to get the raw bodyapp.post( "/webhooks/align", express.raw({ type: "application/json" }), (req, res) => { // Get signature from header const signature = req.headers["x-hmac-signature"] as string; if (!signature) { return res.status(401).send("Missing signature"); } // Get raw body as string const payload = req.body.toString("utf8"); // Verify the signature const isValid = align.webhooks.verifySignature(payload, signature); if (!isValid) { console.error("Invalid webhook signature!"); return res.status(401).send("Invalid signature"); } // Parse and process the event const event = JSON.parse(payload); console.log(`Received: ${event.event_type}`); // Handle different event types switch (event.event_type) { case "offramp_transfer.status.updated": handleTransferStatusUpdate(event.data); break; case "virtual_account.created": handleAccountCreated(event.data); break; case "customer.kycs.updated": handleKycUpdate(event.data); break; } res.status(200).send("OK"); });app.listen(3000);
You must use the raw request body exactly as received. Parsing the JSON first and re-stringifying it may change the byte order, causing verification to fail.
Copy
// Correct - use raw bodyapp.post("/webhooks", express.raw({ type: "application/json" }), ...);// Wrong - JSON parsed firstapp.post("/webhooks", express.json(), (req, res) => { const payload = JSON.stringify(req.body); // May differ from original!});
Use timing-safe comparison
The SDK uses crypto.timingSafeEqual() internally to prevent timing attacks. Never implement your own signature comparison with ===.