Webhooks
Signature Verification

Signature Verification

⚠️

Always verify webhook signatures before processing the payload. This ensures the request is genuinely from Ignite and hasn't been tampered with.

Why Verify Signatures?

Webhook endpoints are publicly accessible URLs. Without signature verification, anyone could send fake events to your endpoint. Signature verification ensures:

  • Authenticity — The request comes from Ignite
  • Integrity — The payload hasn't been modified in transit

Signature Format

Each webhook request includes an X-Webhook-Signature header with the following format:

t=1705316400000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ComponentDescription
tUnix timestamp in milliseconds when the signature was generated
v1HMAC-SHA256 signature (hex encoded)

Verification Algorithm

The signature is computed over a combination of the timestamp and the raw JSON body:

signed_payload = timestamp + "." + JSON.stringify(body)
signature = HMAC-SHA256(signed_payload, signing_secret)

Implementation

const crypto = require('crypto');
 
function verifyWebhookSignature(body, signatureHeader, secret) {
  // Parse header: "t=1234567890,v1=abc123..."
  const parts = signatureHeader.split(',');
  const timestampPart = parts.find(p => p.startsWith('t='));
  const signaturePart = parts.find(p => p.startsWith('v1='));
  
  if (!timestampPart || !signaturePart) {
    return false;
  }
  
  const timestamp = timestampPart.split('=')[1];
  const receivedSignature = signaturePart.split('=')[1];
  
  // Calculate expected signature
  const payloadString = JSON.stringify(body);
  const signedPayload = `${timestamp}.${payloadString}`;
  const hmac = crypto.createHmac('sha256', secret);
  const calculatedSignature = hmac.update(signedPayload).digest('hex');
  
  // Timing-safe comparison
  try {
    return crypto.timingSafeEqual(
      Buffer.from(receivedSignature),
      Buffer.from(calculatedSignature)
    );
  } catch {
    return false;
  }
}
 
// Usage in Express.js
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  
  if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process the webhook...
  res.status(200).json({ received: true });
});

Timestamp Validation (Optional)

To prevent replay attacks, you can also validate that the timestamp is recent:

function verifyWebhookSignature(body, signatureHeader, secret, toleranceMs = 300000) {
  const parts = signatureHeader.split(',');
  const timestampPart = parts.find(p => p.startsWith('t='));
  
  if (!timestampPart) return false;
  
  const timestamp = parseInt(timestampPart.split('=')[1], 10);
  const now = Date.now();
  
  // Reject if timestamp is more than 5 minutes old (default)
  if (Math.abs(now - timestamp) > toleranceMs) {
    return false;
  }
  
  // ... continue with signature verification
}

Common Issues

Signature Mismatch

If signature verification fails, check the following:

  1. Correct Secret — Ensure you're using the signing secret from the webhook subscription, not an API token
  2. Raw Body — The signature is computed over the exact JSON string. Middleware that modifies the body (like pretty-printing) will cause verification to fail
  3. JSON Serialization — Use compact JSON serialization without extra whitespace

Express.js Raw Body Access

If you're using Express.js, make sure to access the raw body for signature verification:

// Use express.json() but also keep raw body
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

Then use req.rawBody instead of JSON.stringify(req.body) for signature verification to ensure exact byte matching.