Webhook Security
Verify webhook signatures with HMAC-SHA256, prevent replay attacks, and secure your endpoints against malicious requests.
How Webhook Signatures Work
SnipLink signs every webhook payload using HMAC-SHA256 with your webhook secret:
- We concatenate the timestamp and request body
- We compute an HMAC-SHA256 hash using your secret
- We send the signature and timestamp in request headers
- Your server recomputes the hash and compares it
If the signatures match, the request is authentic.
Signature Headers
Every webhook request includes these headers:
POST /webhooks/sniplink HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-Sniplink-Webhook-Signature: v1=a8f7d6e5c4b3a2918273645f5e4d3c2b1a0f9e8d7c6b5a4938271605f4e3d2c1
X-Sniplink-Webhook-Timestamp: 1707225600X-Sniplink-Webhook-Signature: The HMAC-SHA256 signature (prefixed withv1=)X-Sniplink-Webhook-Timestamp: Unix timestamp when the webhook was sent
Verification Implementation
Node.js (Express)
const crypto = require(039;crypto039;);
const WEBHOOK_SECRET = process.env.SNIPLINK_WEBHOOK_SECRET;
function verifyWebhookSignature(req) {
const signature = req.headers[039;x-sniplink-webhook-signature039;];
const timestamp = req.headers[039;x-sniplink-webhook-timestamp039;];
const body = req.rawBody; // Important: use raw body, not parsed JSON
// 1. Check timestamp is recent (prevent replay attacks)
const now = Math.floor(Date.now() / 1000);
const timestampAge = now - parseInt(timestamp);
if (timestampAge > 300) { // 5 minutes
console.error(039;Webhook timestamp too old039;);
return false;
}
// 2. Compute expected signature
const payload = `${timestamp}.${body}`;
const expectedSignature = crypto
.createHmac(039;sha256039;, WEBHOOK_SECRET)
.update(payload)
.digest(039;hex039;);
// 3. Compare signatures (timing-safe comparison)
const receivedSignature = signature.replace(039;v1=039;, 039;039;);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
// Express middleware
app.post(039;/webhooks/sniplink039;, express.raw({ type: 039;application/json039; }), (req, res) => {
// Store raw body for signature verification
req.rawBody = req.body.toString(039;utf8039;);
// Parse JSON after storing raw body
req.body = JSON.parse(req.rawBody);
// Verify signature
if (!verifyWebhookSignature(req)) {
return res.status(401).json({ error: 039;Invalid signature039; });
}
// Process webhook
console.log(039;Verified webhook:039;, req.body.type);
res.status(200).json({ received: true });
});Python (Flask)
import hmac
import hashlib
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'
def verify_webhook_signature():
signature = request.headers.get('X-Sniplink-Webhook-Signature')
timestamp = request.headers.get('X-Sniplink-Webhook-Timestamp')
body = request.get_data(as_text=True)
# 1. Check timestamp is recent
now = int(time.time())
timestamp_age = now - int(timestamp)
if timestamp_age > 300: # 5 minutes
return False
# 2. Compute expected signature
payload = f"{timestamp}.{body}"
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 3. Compare signatures (timing-safe)
received_signature = signature.replace('v1=', '')
return hmac.compare_digest(expected_signature, received_signature)
@app.route('/webhooks/sniplink', methods=['POST'])
def handle_webhook():
# Verify signature
if not verify_webhook_signature():
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook
event = request.get_json()
print(f"Verified webhook: {event['type']}")
return jsonify({'received': True}), 200PHP
<?php
$webhookSecret = getenv('SNIPLINK_WEBHOOK_SECRET');
function verifyWebhookSignature() {
global $webhookSecret;
$signature = $_SERVER['HTTP_X_SNIPLINK_WEBHOOK_SIGNATURE'];
$timestamp = $_SERVER['HTTP_X_SNIPLINK_WEBHOOK_TIMESTAMP'];
$body = file_get_contents(039;php://input');
// 1. Check timestamp is recent
$now = time();
$timestampAge = $now - intval($timestamp);
if ($timestampAge > 300) { // 5 minutes
return false;
}
// 2. Compute expected signature
$payload = $timestamp . '.' . $body;
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
// 3. Compare signatures (timing-safe)
$receivedSignature = str_replace('v1=', '', $signature);
return hash_equals($expectedSignature, $receivedSignature);
}
// Handle webhook
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verifyWebhookSignature()) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$event = json_decode(file_get_contents(039;php://input'), true);
error_log('Verified webhook: ' . $event['type']);
http_response_code(200);
echo json_encode(['received' => true]);
}
?>Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
const webhookSecret = "your_webhook_secret"
func verifyWebhookSignature(r *http.Request, body []byte) bool {
signature := r.Header.Get("X-Sniplink-Webhook-Signature")
timestamp := r.Header.Get("X-Sniplink-Webhook-Timestamp")
// 1. Check timestamp is recent
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
if now-ts > 300 { // 5 minutes
return false
}
// 2. Compute expected signature
payload := fmt.Sprintf("%s.%s", timestamp, string(body))
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write([]byte(payload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// 3. Compare signatures (timing-safe)
receivedSignature := strings.TrimPrefix(signature, "v1=")
return subtle.ConstantTimeCompare(
[]byte(expectedSignature),
[]byte(receivedSignature),
) == 1
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Read body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Cannot read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Verify signature
if !verifyWebhookSignature(r, body) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Parse event
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
fmt.Printf("Verified webhook: %s\n", event["type"])
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}Replay Attack Prevention
The timestamp header prevents replay attacks. Always check that the timestamp is within an acceptable range:
- Recommended: ±5 minutes (300 seconds)
- Minimum: ±1 minute (allows for clock skew)
- Maximum: ±15 minutes (only if needed)
Secure Webhook Endpoints
1. Always Use HTTPS
Webhook URLs must use HTTPS. HTTP endpoints are rejected by SnipLink.
✅ https://your-domain.com/webhooks/sniplink
❌ http://your-domain.com/webhooks/sniplink (rejected)2. Keep Secrets Secure
- Store webhook secrets in environment variables, not in code
- Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
- Never commit secrets to version control
- Rotate secrets periodically (every 90 days)
3. Rate Limiting
Implement rate limiting to prevent abuse:
const rateLimit = require(039;express-rate-limit039;);
const webhookLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // Max 100 requests per minute
message: 039;Too many webhook requests039;,
standardHeaders: true,
legacyHeaders: false,
});
app.post(039;/webhooks/sniplink039;, webhookLimiter, handleWebhook);4. IP Whitelisting (Optional)
For additional security, you can whitelist SnipLink's webhook IP addresses. Contact support for the current IP ranges.
Testing Signature Verification
Test your verification implementation with this example:
const crypto = require(039;crypto039;);
// Test values
const secret = 039;test_secret_key039;;
const timestamp = 039;1707225600039;;
const body = 039;{"id":"evt_123","type":"link.created","data":{}}039;;
// Compute signature
const payload = `${timestamp}.${body}`;
const signature = crypto
.createHmac(039;sha256039;, secret)
.update(payload)
.digest(039;hex039;);
console.log(039;Signature:039;, 039;v1=039; + signature);
// Test webhook request
const testRequest = {
headers: {
039;x-sniplink-webhook-signature039;: 039;v1=039; + signature,
039;x-sniplink-webhook-timestamp039;: timestamp,
},
rawBody: body,
};
// Should return true
console.log(039;Verification:039;, verifyWebhookSignature(testRequest));Common Security Mistakes
❌ Don't: Parse JSON Before Verification
// WRONG: Parsing changes the body
app.post(039;/webhooks039;, express.json(), (req, res) => {
verifySignature(JSON.stringify(req.body)); // Different from original!
});
// CORRECT: Verify raw body first
app.post(039;/webhooks039;, express.raw({ type: 039;application/json039; }), (req, res) => {
const rawBody = req.body.toString(039;utf8039;);
verifySignature(rawBody); // Original body
const event = JSON.parse(rawBody);
});❌ Don't: Use String Comparison
// WRONG: Vulnerable to timing attacks
if (expectedSignature === receivedSignature) { ... }
// CORRECT: Use timing-safe comparison
if (crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
)) { ... }❌ Don't: Skip Timestamp Validation
// WRONG: Allows replay attacks
if (signatureValid) {
processWebhook(event);
}
// CORRECT: Check timestamp too
if (signatureValid && timestampRecent) {
processWebhook(event);
}Monitoring & Alerting
Set up monitoring for security events:
- Failed verifications: Alert after 5 consecutive failures
- Old timestamps: Log warnings for timestamps outside window
- Rate limits: Alert when rate limit is hit frequently
- Unusual patterns: Monitor for spikes in webhook traffic
Security Checklist
Before deploying to production:
- ✅ Webhook signature verification implemented
- ✅ Timestamp validation (±5 minutes max)
- ✅ Timing-safe signature comparison
- ✅ HTTPS endpoint (not HTTP)
- ✅ Secrets stored securely (environment variables/vault)
- ✅ Rate limiting enabled
- ✅ Monitoring and alerts configured
- ✅ Error handling for invalid signatures
- ✅ Tested with example payloads
What's Next?
← Setup Guide
Learn how to create and register webhook endpoints
Event Types Reference →
Complete reference for all webhook event types and payloads
Make.com Integration →
Secure webhooks are automatically verified in Make.com
API Reference →
Manage webhooks programmatically via API