Webhook Security
Verify that webhook requests are genuinely from Meeva using HMAC SHA-256 signatures.
Signature Header
Every webhook request from Meeva includes a signature header for verification:
Meeva-Signature: sha256=abc123...This is an HMAC SHA-256 signature of the raw request body, using your webhook secret as the signing key.
Verification Steps
Extract the Signature
Get the Meeva-Signature header from the incoming request
Get Your Webhook Secret
Your secret starts with whsec_ — find it in your dashboard under Settings → Webhooks
Compute the HMAC
Calculate HMAC SHA-256 of the raw request body using your secret
Compare Signatures
If your computed signature matches the header, the request is authentic
Code Examples
const crypto = require('crypto');
const express = require('express');
const app = express();
// Middleware to capture raw body
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
// Verification function
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from('sha256=' + expected)
);
}
// Webhook endpoint
app.post('/webhooks/meeva', (req, res) => {
const signature = req.headers['meeva-signature'];
const secret = process.env.MEEVA_WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.rawBody, signature, secret)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Signature valid - process the event
const event = req.body;
console.log('Verified event:', event.type);
res.status(200).send('OK');
});import hmac
import hashlib
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_your_secret_here'
def verify_signature(payload, signature, secret):
"""Verify HMAC SHA-256 signature"""
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
expected_sig = f'sha256={expected}'
return hmac.compare_digest(signature, expected_sig)
@app.route('/webhooks/meeva', methods=['POST'])
def handle_webhook():
signature = request.headers.get('Meeva-Signature', '')
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
# Signature valid - process the event
event = request.json
print(f"Verified event: {event['type']}")
return 'OK', 200<?php
$webhookSecret = 'whsec_your_secret_here';
// Get raw POST body
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_MEEVA_SIGNATURE'] ?? '';
// Compute expected signature
$expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);
// Verify signature (timing-safe comparison)
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
// Signature valid - process the event
$event = json_decode($payload, true);
error_log('Verified event: ' . $event['type']);
http_response_code(200);
echo 'OK';
?>Security Best Practices
Use timing-safe comparison
Prevents timing attacks on signature verification
Store secrets securely
Use environment variables, never hardcode
Reject invalid signatures
Never process webhooks with bad signatures
Log verification failures
Monitor for potential attacks