Skip to main content

Security & Authentication

Every webhook request includes a signature so you can verify it came from Kula.

Request Headers

HeaderDescription
X-Kula-SignatureHMAC-SHA256 signature
X-Kula-TimestampUnix timestamp
X-Kula-Event-IdUnique event identifier
X-Kula-EventEvent 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.