Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
E-signatures
Categories

How do I verify an e-signature webhook is authentic?

An e-signature webhook is just an HTTP POST to a URL you expose, so anyone who learns that URL can forge a completed-signature event. Before you trust the payload, verify the HMAC signature your provider sends in a request header. Here is the pattern in Node.js with Express:

const crypto = require('crypto');
const express = require('express');
const app = express();

// Capture the RAW body so the bytes match what the provider signed.
app.use('/webhooks/esign', express.raw({ type: '*/*' }));

const SIGNING_SECRET = process.env.WEBHOOK_SIGNING_SECRET;

function isValidSignature(rawBody, receivedSignature) {
  if (!receivedSignature) return false;
  const expected = crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(receivedSignature);
  // timingSafeEqual throws if the buffers differ in length, so guard first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/esign', (req, res) => {
  // The exact header name varies by provider. Check your provider's docs.
  const signature = req.get('X-Signature');
  if (!isValidSignature(req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // Safe to act on event.type now.
  res.status(200).send('ok');
});

Your provider computes an HMAC-SHA256 of the exact request body using a shared secret that only the two of you know, then sends the result in a header. You recompute the same HMAC and compare. A request forged without the secret produces a different hash, fails the check, and gets a 401.

The first thing that trips people up is hashing the wrong bytes. If a JSON body parser runs first and you stringify the object again, key order and whitespace can shift, the bytes no longer match, and every signature fails. That is why the middleware above uses express.raw, and JSON.parse runs only after the check passes.

The second is comparing with plain equality. Use a constant-time comparison such as crypto.timingSafeEqual instead of the equality operator, because an early-exit compare can leak, through timing, how many leading characters matched and help an attacker recover the signature. Finally, confirm whether your provider hex-encodes or base64-encodes the digest and what it names the header, since both vary between services.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms