Skip to main content

Signature Verification

All webhook requests are signed using HMAC-SHA256. Always verify signatures before processing events.

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
PartDescription
tUnix timestamp when signed
v1HMAC-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​

  1. Always verify signatures — Never trust unverified payloads
  2. Use timing-safe comparison — Prevent timing attacks
  3. Check timestamp — Reject events older than 5 minutes
  4. Use raw body — Don't parse JSON before verification
  5. Store secret securely — Use environment variables