Security & Authentication
Every webhook request includes a signature so you can verify it came from Kula.
Request Headers
| Header | Description |
|---|---|
X-Kula-Signature | HMAC-SHA256 signature |
X-Kula-Timestamp | Unix timestamp |
X-Kula-Event-Id | Unique event identifier |
X-Kula-Event | Event type |
Signature Format
The signature header contains a timestamp and signature:
X-Kula-Signature: t=1642253600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Verifying Signatures
Step 1: Extract Values
Parse the timestamp and signature from the header:
const signature = req.headers['x-kula-signature'];
const [tPart, v1Part] = signature.split(',');
const timestamp = tPart.replace('t=', '');
const receivedSig = v1Part.replace('v1=', '');
Step 2: Compute Expected Signature
Create the signed payload string and compute HMAC-SHA256:
const crypto = require('crypto');
const payload = `${timestamp}.${JSON.stringify(req.body)}`;
const expectedSig = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
Step 3: Compare Signatures
Use constant-time comparison to prevent timing attacks:
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(receivedSig)
);
Complete Example
const crypto = require('crypto');
function verifyWebhook(req, webhookSecret) {
const signature = req.headers['x-kula-signature'];
if (!signature) return false;
const [tPart, v1Part] = signature.split(',');
const timestamp = tPart.replace('t=', '');
const receivedSig = v1Part.replace('v1=', '');
// Check timestamp (5 minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false;
}
// Compute expected signature
const payload = `${timestamp}.${JSON.stringify(req.body)}`;
const expectedSig = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
// Compare signatures
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(receivedSig)
);
}
app.post('/webhooks/kula', (req, res) => {
if (!verifyWebhook(req, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
res.status(200).send('OK');
});
Timestamp Validation
Reject requests with timestamps older than 5 minutes to prevent replay attacks.
Rotating Secrets
To rotate your webhook secret:
curl -X POST "https://api.kula.ai/v1/webhooks/{id}/regenerate-secret" \
-H "Authorization: Bearer your_api_token_here"
Update your application with the new secret before the next delivery.