Security

Learn how to secure your webhook endpoints and validate webhook signatures.

Signature Verification

Every webhook request includes an X-Zellify-Signature header containing an HMAC SHA-256 hash of the raw request body computed using your webhook secret.

Always validate webhook signatures before processing any webhook data.

JavaScript / TypeScript (Node.js)

import crypto from 'crypto';
import express from 'express';

function verifyWebhookSignature(rawBody: Buffer, signature: string, secret: string): boolean {
  const computed = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(computed, 'hex');
  const b = Buffer.from(signature || '', 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

// Capture raw body for signature verification
app.use(express.json({
  verify: (req: any, _res, buf) => { req.rawBody = buf; }
}));

app.post('/webhooks', (req: any, res) => {
  const signature = req.header('X-Zellify-Signature') || '';
  const isValid = verifyWebhookSignature(req.rawBody, signature, process.env.ZELLIFY_WEBHOOK_SECRET || '');
  if (!isValid) return res.status(401).json({ error: 'Invalid signature' });
  // Process the webhook...
  res.status(200).json({ received: true });
});

Python (Flask)

import hmac
import hashlib
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    computed = hmac.new(secret.encode('utf-8'), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, signature or '')

@app.route('/webhooks', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Zellify-Signature', '')
    payload = request.get_data()  # raw bytes
    secret = os.environ.get('ZELLIFY_WEBHOOK_SECRET', '')

    if not verify_webhook_signature(payload, signature, secret):
        return jsonify({'error': 'Invalid signature'}), 401

    # Process the webhook...
    return jsonify({'received': True}), 200

PHP

<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ZELLIFY_SIGNATURE'] ?? '';
$secret = getenv('ZELLIFY_WEBHOOK_SECRET') ?: '';

$computed = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($computed, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

http_response_code(200);
echo json_encode(['received' => true]);

Best practices:

  • Validate the signature using the raw request body (not parsed JSON)

  • Use timing-safe comparison (timingSafeEqual, compare_digest, hash_equals)

  • Keep your secret in environment variables and rotate periodically

Last updated