Skip to main content

Overview

All Daya webhooks include an X-Daya-Signature header containing an HMAC-SHA256 signature of the payload. Always verify this signature to ensure the webhook came from Daya.
Never process unverified webhooks. Attackers could send fake webhooks to manipulate your system.

Signature Header

X-Daya-Signature: a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8

Verification Algorithm

  1. Get raw request body as string
  2. Compute HMAC-SHA256 using your webhook secret
  3. Compare computed signature with X-Daya-Signature header
  4. Use timing-safe comparison to prevent timing attacks

Implementation Examples

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  
  // Timing-safe comparison
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch (error) {
    return false;
  }
}

// Express.js middleware
app.post('/webhooks/daya', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-daya-signature'];
  const payload = req.body.toString('utf8');
  
  if (!verifyWebhookSignature(payload, signature, process.env.DAYA_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process webhook...
  const event = JSON.parse(payload);
  res.status(200).send('OK');
});

Important Notes

Critical: Compute HMAC on the raw request body before parsing JSON. Parsing changes whitespace and ordering, breaking the signature.
// ✅ Correct: Use raw body
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body.toString('utf8');
  verify(payload, signature, secret);
});

// ❌ Wrong: JSON.stringify changes format
app.post('/webhooks', express.json(), (req, res) => {
  const payload = JSON.stringify(req.body); // Wrong!
  verify(payload, signature, secret);
});
Regular string comparison (==) is vulnerable to timing attacks. Use constant-time comparison:
  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • Go: hmac.Equal()
  • PHP: hash_equals()
  • Store webhook secret in environment variables
  • Never commit secrets to version control
  • Rotate secrets regularly
  • Use different secrets for sandbox and production

Testing Verification

Generate test signatures for local testing:
# Generate test signature
echo -n '{"event":"deposit.settled","event_id":"evt_test"}' | \
  openssl dgst -sha256 -hmac "your_webhook_secret" | \
  awk '{print $2}'

Common Issues

Possible causes:
  • Using wrong webhook secret
  • Not using raw request body
  • Character encoding issues
Debug:
console.log('Received signature:', signature);
console.log('Expected signature:', expectedSignature);
console.log('Payload:', payload);
console.log('Secret (first 4 chars):', secret.substring(0, 4));
Cause: Parsing JSON before verificationFix: Always compute HMAC on raw body, then parse JSON

Next Steps