Overview
Every webhook Yuno sends includes an HMAC-SHA256 signature in the request headers. Verifying this signature ensures the webhook originated from Yuno and has not been tampered with in transit.
Always verify webhook signatures before processing events. Unverified webhooks could be spoofed by malicious actors to trigger unauthorized actions in your system.
How Signature Verification Works
- Yuno computes an HMAC-SHA256 hash of the raw request body using your signing secret
- The hash is included in the
x-yuno-signature header
- Your server recomputes the hash and compares it to the header value
- If they match, the webhook is authentic
Retrieve Your Signing Secret
Find your webhook signing secret in Dashboard > Settings > Webhooks. Click on your endpoint to reveal the secret. Store it securely as an environment variable.
YUNO_WEBHOOK_SECRET=whsec_your_signing_secret_here
Implementation
const crypto = require('crypto');
function verifyWebhookSignature(req) {
const signature = req.headers['x-yuno-signature'];
const timestamp = req.headers['x-yuno-timestamp'];
const body = JSON.stringify(req.body);
// Construct the signed payload
const signedPayload = `${timestamp}.${body}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', process.env.YUNO_WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware
app.post('/webhooks/yuno', express.json(), (req, res) => {
if (!verifyWebhookSignature(req)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Signature valid, process the event
handleEvent(req.body);
res.status(200).send('OK');
});
Timestamp Validation
In addition to signature verification, validate the timestamp to prevent replay attacks:
function isTimestampValid(timestamp, toleranceSeconds = 300) {
const webhookTime = parseInt(timestamp, 10);
const currentTime = Math.floor(Date.now() / 1000);
return Math.abs(currentTime - webhookTime) <= toleranceSeconds;
}
A tolerance of 5 minutes (300 seconds) is recommended. Webhooks older than this window should be rejected to prevent replay attacks.
Common Issues
| Issue | Cause | Solution |
|---|
| Signature mismatch | Parsing body before verification | Verify against raw body, not parsed JSON |
| Missing header | Incorrect endpoint configuration | Check Dashboard webhook URL matches |
| Wrong secret | Using production secret in sandbox | Ensure environment-specific secrets |
Testing Signature Verification
Generate a test signature locally to validate your implementation:
const crypto = require('crypto');
const secret = 'whsec_test_secret';
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ type: 'payment.succeeded', data: {} });
const signedPayload = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
console.log('Signature:', signature);
console.log('Timestamp:', timestamp);