Skip to main content

Overview

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

Signature Header

X-Webhook-Signature: sha256=a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8

Additional Headers

Daya also sends these headers with every webhook request:
HeaderDescription
X-Webhook-EventEvent type (e.g., order.filled)
X-Webhook-IDUnique event identifier (UUID)
X-Webhook-TimestampEvent timestamp (RFC3339)
User-AgentDaya-Webhook/1.0

Verification Algorithm

  1. Get raw request body as string
  2. Compute HMAC-SHA256 using your webhook secret
  3. Strip the sha256= prefix from X-Webhook-Signature header
  4. Compare computed signature with the extracted signature
  5. 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-pro', express.raw({ type: 'application/json' }), (req, res) => {
  const signatureHeader = req.headers['x-webhook-signature'] || '';
  const signature = signatureHeader.replace('sha256=', ''); // Strip prefix
  const payload = req.body.toString('utf8');

  if (!verifyWebhookSignature(payload, signature, process.env.DAYA_PRO_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 different environments

Testing Verification

Generate test signatures for local testing:
# Generate test signature
echo -n '{"event":"order.filled","event_id":"evt_pro_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 length:', payload.length);
console.log('Secret (first 4 chars):', secret.substring(0, 4));
Cause: Parsing JSON before verificationFix: Always compute HMAC on raw body, then parse JSON
Cause: This shouldn’t happen - same payload = same signatureDebug: Log and compare payloads between requests

Next Steps

Webhook Overview

Delivery guarantees and implementation

Webhook Events

Event schemas and payloads