Skip to main content

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

  1. Yuno computes an HMAC-SHA256 hash of the raw request body using your signing secret
  2. The hash is included in the x-yuno-signature header
  3. Your server recomputes the hash and compares it to the header value
  4. 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

IssueCauseSolution
Signature mismatchParsing body before verificationVerify against raw body, not parsed JSON
Missing headerIncorrect endpoint configurationCheck Dashboard webhook URL matches
Wrong secretUsing production secret in sandboxEnsure 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);