Every webhook request includes two headers for signature verification:
| Header | Description |
|---|
X-Klara-Signature | sha256=<hex-digest> — HMAC-SHA256 signature of the payload |
X-Klara-Timestamp | Unix timestamp (seconds) when the webhook was sent |
Verification steps
- Extract the timestamp and signature from headers
- Reject timestamps older than 5 minutes (replay protection)
- Compute
HMAC-SHA256(secret, "{timestamp}.{raw_body}")
- Compare the computed signature with the received one using constant-time comparison
Always use a constant-time comparison function (e.g., crypto.timingSafeEqual in Node.js) to prevent timing attacks.
Implementation examples
import crypto from 'crypto';
const TIMESTAMP_TOLERANCE = 300; // 5 minutes in seconds
function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
const timestamp = parseInt(timestampHeader, 10);
// 1. Reject stale timestamps
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > TIMESTAMP_TOLERANCE) {
throw new Error('Webhook timestamp too old');
}
// 2. Compute expected signature
const signedContent = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
// 3. Constant-time comparison
const received = signatureHeader.replace('sha256=', '');
const isValid = crypto.timingSafeEqual(
Buffer.from(received),
Buffer.from(expected)
);
if (!isValid) {
throw new Error('Invalid webhook signature');
}
return true;
}
// Express example
app.post('/webhooks/klara', express.raw({ type: 'application/json' }), (req, res) => {
try {
verifyWebhookSignature(
req.body.toString(),
req.headers['x-klara-signature'],
req.headers['x-klara-timestamp'],
process.env.KLARA_WEBHOOK_SECRET
);
const event = JSON.parse(req.body);
// Process event...
res.status(200).send('OK');
} catch (err) {
res.status(401).send('Invalid signature');
}
});
Make sure you use the raw request body (not a parsed/re-serialized version) when computing the signature. Re-serializing JSON can change key ordering or whitespace, which will cause verification to fail.
Testing signatures
Use the Send test button on the webhook settings page to send a test event to your endpoint. Check that your signature verification passes before subscribing to real events.