Webhooks — Node.js SDK
Verify and handle webhooks
The SDK provides a built-in method to verify webhook signatures and parse events:
import express from 'express';
import { Simiz } from '@simiz/node-sdk';
const app = express();
const simiz = new Simiz(process.env.SIMIZ_SECRET_KEY);
app.post('/webhooks/simiz',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-simiz-signature'] as string;
try {
const event = simiz.webhooks.constructEvent(
req.body,
signature,
process.env.SIMIZ_WEBHOOK_SECRET
);
switch (event.type) {
case 'payment.succeeded':
const payment = event.data;
console.log('Payment received:', payment.id);
// Update order status in your database
break;
case 'payment.failed':
console.log('Payment failed:', event.data.id);
// Notify customer, offer retry
break;
case 'refund.succeeded':
console.log('Refund completed:', event.data.id);
// Update order and notify customer
break;
case 'subscription.renewed':
console.log('Subscription renewed:', event.data.id);
// Extend subscription access
break;
default:
console.log('Unhandled event type:', event.type);
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
Important: Use express.raw() instead of express.json() for the webhook route. The signature verification requires the raw request body.
Manual signature verification
If you prefer to verify signatures manually:
Signature format: X-Simiz-Signature: t=<timestamp>,v1=<hmac_sha256_hex>The timestamp protects against replay attacks. Signatures older than 5 minutes are rejected.
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string,
toleranceSeconds: number = 300
): boolean {
// Parse signature header: t=<timestamp>,v1=<hash>
const parts = signatureHeader.split(',');
let timestamp: number | undefined;
let hash: string | undefined;
for (const part of parts) {
if (part.startsWith('t=')) {
timestamp = parseInt(part.slice(2), 10);
} else if (part.startsWith('v1=')) {
hash = part.slice(3);
}
}
if (timestamp === undefined || !hash) {
return false;
}
// Check timestamp is within tolerance (default: 5 minutes)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) {
return false;
}
// Calculate expected signature
const signaturePayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signaturePayload)
.digest('hex');
// Timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(hash, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
} catch {
return false;
}
}
Best practices
- Respond with
200 immediately — Process events asynchronously with a job queue
- Handle duplicates — Use the event
id to check if you’ve already processed this event
- Log raw payloads — Store the raw JSON for debugging failed webhook processing
- Use a dedicated route — Don’t mix webhook handling with other API routes