Documenso

Webhook Verification

Verify webhook signatures for security.

Overview

Verifying webhook requests ensures that incoming payloads originate from Documenso and have not been tampered with. Without verification, attackers could forge requests to your endpoint and trigger unintended actions.

How Documenso Signs Webhooks

When you configure a webhook with a secret, Documenso includes that secret in every webhook request via the X-Documenso-Secret header. Your server should compare this header value against your stored secret to authenticate the request.

POST /your-webhook-endpoint HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-Documenso-Secret: your_webhook_secret_here

{"event": "DOCUMENT_COMPLETED", "payload": {...}}

Signature Header Format

HeaderDescription
X-Documenso-SecretThe secret key you configured when creating the webhook

The header contains your webhook secret as a plain string. If you did not configure a secret, the header will be an empty string.

Verification Steps

Extract the X-Documenso-Secret header from the incoming request

Compare it against your stored webhook secret using a constant-time comparison

Reject the request if the values do not match

Process the webhook payload if verification succeeds

Always use constant-time string comparison to prevent timing attacks. Standard equality operators (=== or ==) can leak information about the secret through response time variations.

Code Examples

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

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET;

function verifyWebhookSignature(receivedSecret, expectedSecret) {
  if (!expectedSecret) {
    // No secret configured, skip verification
    // Not recommended for production
    return true;
  }

  if (!receivedSecret) {
    return false;
  }

  // Use constant-time comparison to prevent timing attacks
  try {
    return crypto.timingSafeEqual(
      Buffer.from(receivedSecret),
      Buffer.from(expectedSecret),
    );
  } catch {
    return false;
  }
}

app.post('/webhooks/documenso', (req, res) => {
  const receivedSecret = req.headers['x-documenso-secret'];

  if (!verifyWebhookSignature(receivedSecret, WEBHOOK_SECRET)) {
    console.error('Webhook verification failed');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature verified, process the webhook
  const { event, payload } = req.body;
  console.log(`Verified webhook: ${event}`);

  // Process the event...

  res.status(200).json({ received: true });
});

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});
import hmac
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = os.environ.get('DOCUMENSO_WEBHOOK_SECRET')

def verify_webhook_signature(received_secret, expected_secret):
    """Verify the webhook signature using constant-time comparison."""
    if not expected_secret:
        # No secret configured, skip verification
        # Not recommended for production
        return True

    if not received_secret:
        return False

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(received_secret, expected_secret)

@app.route('/webhooks/documenso', methods=['POST'])
def handle_webhook():
    received_secret = request.headers.get('X-Documenso-Secret', '')

    if not verify_webhook_signature(received_secret, WEBHOOK_SECRET):
        print('Webhook verification failed')
        return jsonify({'error': 'Invalid signature'}), 401

    # Signature verified, process the webhook
    data = request.get_json()
    event = data.get('event')
    payload = data.get('payload')

    print(f'Verified webhook: {event}')

    # Process the event...

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Handling Verification Failures

When verification fails, follow these practices:

ActionDescription
Return 401 statusRespond with 401 Unauthorized to indicate authentication failure
Log the attemptRecord failed attempts for security monitoring
Do not processNever process the payload if verification fails
Do not leak detailsAvoid exposing information about why verification failed
app.post('/webhooks/documenso', (req, res) => {
  const receivedSecret = req.headers['x-documenso-secret'];

  if (!verifyWebhookSignature(receivedSecret, WEBHOOK_SECRET)) {
    // Log for monitoring but don't expose details
    console.error('Webhook verification failed', {
      timestamp: new Date().toISOString(),
      ip: req.ip,
    });

    // Generic error response
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Continue processing...
});

Common Verification Issues

IssueCauseSolution
Secret mismatchWebhook secret changed or misconfiguredVerify the secret in your environment matches the one in Documenso
Empty headerWebhook created without a secretAdd a secret to the webhook configuration in Documenso
Encoding issuesString encoding mismatchEnsure both secrets use the same encoding (UTF-8)

Security Best Practices

See Also

On this page