Verifying Webhook Signatures
Each webhook request includes anX-Laiki-Signature header. Compute the HMAC-SHA256 signature of the raw request payload using your Webhook Secret and compare it using constant-time equality check.
Headers
X-Laiki-Signature: 8d9a246830cf42e31690184b29bb807a0487...
X-Laiki-Event: card.transaction.completed
Content-Type: application/json
Code Implementations
const crypto = require('crypto');
function verifyWebhook(rawPayloadString, signatureHeader, webhookSecret) {
const hash = crypto
.createHmac('sha256', webhookSecret)
.update(rawPayloadString)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, 'hex'),
Buffer.from(hash, 'hex')
);
}
// Express Route Example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-laiki-signature'];
const isValid = verifyWebhook(req.body.toString('utf8'), sig, process.env.LAIKI_WEBHOOK_SECRET);
if (!isValid) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Received event:', event.event);
res.status(200).json({ received: true });
});
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = "whsec_..."
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature)
@app.post("/webhook")
async def handle_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("x-laiki-signature")
if not signature or not verify_signature(payload, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=400, detail="Invalid signature")
event = await request.json()
print("Received event:", event.get("event"))
return {"received": True}
<?php
$secret = getenv('LAIKI_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_LAIKI_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input');
$computed = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($computed, $signature)) {
http_response_code(400);
exit('Invalid signature');
}
$event = json_decode($payload, true);
http_response_code(200);
echo json_encode(['received' => true]);
?>
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func verifyWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expectedMAC := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedMAC))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Laiki-Signature")
secret := os.Getenv("LAIKI_WEBHOOK_SECRET")
if !verifyWebhook(body, sig, secret) {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received": true}`))
}

