Signature Verification
All webhook requests are signed using HMAC-SHA256. Always verify signatures before processing events.
Using the SDK (Recommended)​
The SDK handles signature verification for you:
import { MaesClient, MaesWebhookSignatureError } from '@nuvoni/maes-sdk';
const client = new MaesClient({ apiKey: 'sk_live_xxxxx' });
// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = client.webhooks.verifySignature(
req.body.toString(),
req.headers['x-webhook-signature'] as string,
process.env.WEBHOOK_SECRET!,
);
// Event is verified and safe to process
switch (event.event) {
case 'card.enabled':
console.log('Card enabled:', event.data.card_id);
break;
case 'card.disabled':
console.log('Card disabled:', event.data.card_id);
break;
}
res.status(200).send('OK');
} catch (error) {
if (error instanceof MaesWebhookSignatureError) {
return res.status(401).send('Invalid signature');
}
throw error;
}
});
Signature Header​
Every webhook request includes the X-Webhook-Signature header:
X-Webhook-Signature: t=1703693400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
| Part | Description |
|---|---|
t | Unix timestamp when signed |
v1 | HMAC-SHA256 signature |
Manual Verification​
If you need to verify manually without the SDK:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string,
): boolean {
// Parse header
const parts = signatureHeader.split(',');
const timestamp = parts.find((p) => p.startsWith('t='))?.replace('t=', '');
const signature = parts.find((p) => p.startsWith('v1='))?.replace('v1=', '');
if (!timestamp || !signature) {
return false;
}
// Construct signed payload
const signedPayload = `${timestamp}.${payload}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex'),
);
} catch {
return false;
}
}
// Usage
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
// Process verified event...
res.status(200).send('OK');
});
Testing Webhooks​
Generate a test signature for local development:
import { MaesClient } from '@nuvoni/maes-sdk';
const client = new MaesClient({ apiKey: 'sk_sandbox_xxxxx' });
const payload = JSON.stringify({
id: 'evt_test123',
event: 'card.enabled',
created_at: new Date().toISOString(),
data: {
card_id: 'card-123',
card_number: '1234567890',
status: 'active',
},
});
const signature = client.webhooks.generateSignature(
payload,
'whsec_your_test_secret',
);
// Use signature in X-Webhook-Signature header for testing
console.log('Signature:', signature);
Security Best Practices​
- Always verify signatures — Never trust unverified payloads
- Use timing-safe comparison — Prevent timing attacks
- Check timestamp — Reject events older than 5 minutes
- Use raw body — Don't parse JSON before verification
- Store secret securely — Use environment variables