Webhook Security & Verification
Learn how to secure your webhook endpoints and verify that requests are genuinely from Ranklite.
Critical Security Notice
Always verify webhook signatures before processing payloads. Without verification, malicious actors could send fake webhooks to your endpoint.
How Webhook Signing Works
Ranklite signs every webhook with a secret key using HMAC-SHA256. Each webhook request includes an X-Ranklite-Signature header containing the signature.
Verification Process:
- Ranklite creates a signature using your webhook secret
- The signature is sent in the request header
- Your server recreates the signature using the same secret
- If signatures match, the webhook is authentic
Implementing Signature Verification
Node.js / Express
import crypto from 'crypto';
import express from 'express';
const app = express();
// Important: Use raw body for signature verification
app.use('/api/webhooks', express.raw({ type: 'application/json' }));
function verifyWebhookSignature(rawBody, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const digest = 'sha256=' + hmac.update(rawBody).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
app.post('/api/webhooks/ranklite', (req, res) => {
const signature = req.headers['x-ranklite-signature'];
const secret = process.env.RANKLITE_WEBHOOK_SECRET;
if (!signature || !verifyWebhookSignature(req.body, signature, secret)) {
console.error('Invalid signature');
return res.status(401).send('Unauthorized');
}
// Parse the verified body
const event = JSON.parse(req.body.toString());
// Process the webhook
console.log('Verified webhook:', event.type);
res.status(200).send('OK');
});Python / Flask
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_signature(payload, signature, secret):
expected_sig = 'sha256=' + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, signature)
@app.route('/api/webhooks/ranklite', methods=['POST'])
def webhook():
signature = request.headers.get('X-Ranklite-Signature')
secret = os.environ.get('RANKLITE_WEBHOOK_SECRET')
if not signature or not verify_signature(request.data, signature, secret):
abort(401)
event = request.get_json()
print(f"Verified webhook: {event['type']}")
return 'OK', 200PHP
<?php
function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedSignature, $signature);
}
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_RANKLITE_SIGNATURE'];
$secret = getenv('RANKLITE_WEBHOOK_SECRET');
if (!verifyWebhookSignature($payload, $signature, $secret)) {
http_response_code(401);
die('Unauthorized');
}
$event = json_decode($payload, true);
error_log("Verified webhook: " . $event['type']);
http_response_code(200);
echo 'OK';
?>Security Best Practices
Always Use HTTPS
Ranklite only sends webhooks to HTTPS endpoints to prevent man-in-the-middle attacks
Store Secrets Securely
Never hardcode webhook secrets. Use environment variables or secure secret management
Use Timing-Safe Comparison
Use constant-time string comparison to prevent timing attacks
Verify Before Processing
Always verify the signature before processing any webhook data
Implement Rate Limiting
Protect your endpoint from abuse with rate limiting
Log Security Events
Log all verification failures for security monitoring
Rotating Webhook Secrets
If you suspect your webhook secret has been compromised, rotate it immediately:
- 1
Generate New Secret
Go to Settings → Webhooks → Edit webhook → Regenerate Secret
- 2
Update Your Endpoint
Update your server with the new secret before the old one expires
- 3
Test Verification
Use the "Test Webhook" button to verify the new secret works
The old secret remains valid for 24 hours after rotation to prevent service disruption.
Testing Signature Verification
You can test your signature verification with this example payload and secret:
Secret: whsec_test_secret_12345
Payload: {"id":"evt_test","type":"article.published","created":1734134400}
Expected Signature: sha256=9c5b94b1e7f3d6c4a8f2e1d3b5c7a9e2f4d6c8b0a1e3c5d7e9b2a4c6d8e0f2