Webhook Signature Validation

How to verify the authenticity of incoming webhook requests

Every webhook request from Caliza includes an HMAC-SHA256 signature so you can verify that the payload is authentic and has not been tampered with. This guide walks through the verification process with implementation examples.

For webhook setup and configuration, see Webhooks. For the full list of events, see Webhook Events.


How it works

  1. When Caliza sends a webhook, we compute an HMAC-SHA256 hash of the raw request body using your webhook secret.
  2. The resulting signature is Base64-encoded and included in the X-Caliza-Webhook-Signature header.
  3. Your application recomputes the signature using the same secret and compares it to the received header.

Verification steps

  1. Extract the signature from the X-Caliza-Webhook-Signature header.
  2. Read the raw request body — it must be the exact, unmodified payload. Do not parse and re-serialize the JSON before verifying.
  3. Compute the HMAC-SHA256 of the raw body using your webhook secret.
  4. Base64-encode the result and compare it to the received signature.

Implementation examples

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
import org.apache.commons.io.IOUtils;

public class WebhookSignatureVerifier {

    public boolean verifySignature(HttpServletRequest httpRequest, String secret)
            throws Exception {
        String payload = IOUtils.toString(httpRequest.getReader());
        String receivedSignature = httpRequest.getHeader("X-Caliza-Webhook-Signature");

        Mac sha256HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
        sha256HMAC.init(secretKey);

        byte[] hash = sha256HMAC.doFinal(payload.getBytes());
        String calculatedSignature = Base64.getEncoder().encodeToString(hash);

        return MessageDigest.isEqual(
            calculatedSignature.getBytes(),
            receivedSignature.getBytes()
        );
    }
}

Node.js (native http)

const http = require('http');
const crypto = require('crypto');

const secret = 'your_webhook_secret';

const server = http.createServer((req, res) => {
  if (req.url === '/webhook' && req.method === 'POST') {
    let rawBody = '';
    req.on('data', chunk => { rawBody += chunk.toString(); });

    req.on('end', () => {
      const receivedSignature = req.headers['x-caliza-webhook-signature'];
      const hmac = crypto.createHmac('sha256', secret);
      hmac.update(rawBody);
      const calculatedSignature = hmac.digest('base64');

      const isValid = calculatedSignature === receivedSignature;

      res.writeHead(isValid ? 200 : 401);
      res.end(JSON.stringify({ valid: isValid }));
    });
  } else {
    res.writeHead(404);
    res.end();
  }
});

server.listen(3000);

Express.js

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const app = express();
const secret = 'your_webhook_secret';

// Capture the raw body before parsing
app.use(bodyParser.json({
  verify: (req, res, buf, encoding) => {
    if (buf && buf.length) {
      req.rawBody = buf.toString(encoding || 'utf8');
    }
  }
}));

app.post('/webhook', (req, res) => {
  const receivedSignature = req.headers['x-caliza-webhook-signature'];
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(req.rawBody);
  const calculatedSignature = hmac.digest('base64');

  const isValid = calculatedSignature === receivedSignature;
  res.status(isValid ? 200 : 401).json({ valid: isValid });
});

app.listen(3000);

Python

import hmac
import hashlib
import base64

def verify_signature(payload: bytes, received_signature: str, secret: str) -> bool:
    computed = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).digest()
    calculated_signature = base64.b64encode(computed).decode()
    return hmac.compare_digest(calculated_signature, received_signature)

Managing your webhook secret

  • View your secret in the Backoffice under Avatar Icon > Profile Page.
  • Regenerate your secret by calling POST /v1/integrators/{integratorId}/update-webhook-secret.
  • Retrieve your encrypted secret by calling GET /v1/integrators/{integratorId}/webhook-secret.

Best practices

  • Store secrets securely. Never hardcode them in source code or store them in plain text. Use environment variables or a secrets manager.
  • Limit access. Only team members who configure webhook processing should have access to the secret.
  • Rotate regularly. Periodically regenerate your secret and update your verification logic.
  • Always verify before processing. Reject any request where the signature does not match — do not process the payload.
  • Use constant-time comparison. The examples above use MessageDigest.isEqual (Java) and hmac.compare_digest (Python) to prevent timing attacks.