Webhook Security
Every webhook delivery is signed with an HMAC-SHA256 signature using your webhook secret. Always verify signatures to ensure payloads are genuinely from inSigner and haven’t been tampered with.
Signature headers
Section titled “Signature headers”Each webhook request includes these security headers:
| Header | Description |
|---|---|
X-InSigner-Signature | sha256=<hex_digest> — HMAC-SHA256 of the raw request body |
X-InSigner-Timestamp | Unix timestamp (seconds) when the event was created |
Verification algorithm
Section titled “Verification algorithm”The signature is computed as:
HMAC-SHA256(webhook_secret, raw_request_body)Implementation examples
Section titled “Implementation examples”import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.INSIGNER_WEBHOOK_SECRET;
function verifyWebhookSignature(req) { const signature = req.headers['x-insigner-signature']; const timestamp = req.headers['x-insigner-timestamp'];
if (!signature || !timestamp) { throw new Error('Missing signature headers'); }
// Prevent replay attacks: reject events older than 5 minutes const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) { throw new Error('Webhook timestamp too old'); }
// Compute expected signature from raw body const expectedSignature = 'sha256=' + crypto .createHmac('sha256', WEBHOOK_SECRET) .update(req.rawBody) // Use raw body, not parsed JSON .digest('hex');
// Constant-time comparison to prevent timing attacks if (!crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) )) { throw new Error('Invalid webhook signature'); }
return true;}
// Express route handlerapp.post('/webhooks/insigner', express.raw({ type: 'application/json' }), (req, res) => { try { // Store raw body before parsing req.rawBody = req.body; verifyWebhookSignature(req);
const event = JSON.parse(req.body); console.log(`Received event: ${event.event}`);
// Process the event switch (event.event) { case 'document.completed': handleDocumentCompleted(event.data); break; case 'signer.completed': handleSignerCompleted(event.data); break; }
res.status(200).json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(401).json({ error: err.message }); }});import hmacimport hashlibimport timeimport osfrom flask import Flask, request, jsonify
app = Flask(__name__)WEBHOOK_SECRET = os.environ["INSIGNER_WEBHOOK_SECRET"]
def verify_webhook_signature(req): signature = req.headers.get("X-InSigner-Signature") timestamp = req.headers.get("X-InSigner-Timestamp")
if not signature or not timestamp: raise ValueError("Missing signature headers")
# Prevent replay attacks now = int(time.time()) if abs(now - int(timestamp)) > 300: raise ValueError("Webhook timestamp too old")
# Compute expected signature from raw body expected = "sha256=" + hmac.new( WEBHOOK_SECRET.encode(), req.get_data(), # Raw bytes, not parsed JSON hashlib.sha256 ).hexdigest()
# Constant-time comparison if not hmac.compare_digest(signature, expected): raise ValueError("Invalid webhook signature")
return True
@app.route("/webhooks/insigner", methods=["POST"])def handle_webhook(): try: verify_webhook_signature(request) event = request.get_json() print(f"Received event: {event['event']}")
if event["event"] == "document.completed": handle_document_completed(event["data"]) elif event["event"] == "signer.completed": handle_signer_completed(event["data"])
return jsonify({"received": True}), 200 except ValueError as e: return jsonify({"error": str(e)}), 401import hmacimport hashlibimport timeimport jsonfrom django.http import JsonResponsefrom django.views.decorators.csrf import csrf_exemptfrom django.conf import settings
@csrf_exemptdef webhook_handler(request): if request.method != "POST": return JsonResponse({"error": "Method not allowed"}, status=405)
signature = request.headers.get("X-InSigner-Signature") timestamp = request.headers.get("X-InSigner-Timestamp")
if not signature or not timestamp: return JsonResponse({"error": "Missing headers"}, status=401)
# Prevent replay attacks if abs(int(time.time()) - int(timestamp)) > 300: return JsonResponse({"error": "Timestamp too old"}, status=401)
# Verify HMAC expected = "sha256=" + hmac.new( settings.INSIGNER_WEBHOOK_SECRET.encode(), request.body, # Raw bytes hashlib.sha256 ).hexdigest()
if not hmac.compare_digest(signature, expected): return JsonResponse({"error": "Invalid signature"}, status=401)
event = json.loads(request.body) # Process event...
return JsonResponse({"received": True})Security best practices
Section titled “Security best practices”-
Always verify signatures. Never process webhook payloads without verifying the HMAC signature.
-
Use constant-time comparison. Use
crypto.timingSafeEqual()(Node.js) orhmac.compare_digest()(Python) to prevent timing attacks. -
Check the timestamp. Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.
-
Use HTTPS. Always use an HTTPS URL for your webhook endpoint so payloads are encrypted in transit.
-
Respond quickly. Return a
2xxresponse within 10 seconds. If you need more processing time, acknowledge the webhook immediately and process asynchronously. -
Handle duplicates. Use the
X-InSigner-Delivery-Idheader to deduplicate events — the same event may be delivered more than once during retries.