Webhook System

Developer Guide

Overview

This webhook system allows you to receive real-time HTTP POST notifications when document events occur in the e-signature platform. Instead of polling our API, we'll push updates directly to your server.


Getting Started

1Configure Your Webhook

  1. Navigate to Settings → API Keys
  2. Select the API key you want to configure webhooks for
  3. Expand the Webhook Configuration section
  4. Enable webhooks with the toggle switch
  5. Enter your webhook endpoint URL (must be HTTPS in production)
  6. Select which events you want to subscribe to
  7. Copy your webhook secret for signature verification
  8. Click Save Configuration
  9. Use the Test Webhook button to verify connectivity

2Webhook Endpoint Requirements

Your webhook endpoint must:

  • Accept HTTP POST requests
  • Return a 2xx status code (200-299) on success
  • Respond within 10 seconds (otherwise timeout)
  • Use HTTPS in production environments
  • Verify the webhook signature (see Security section)

Webhook Events

The system supports the following event types:

Event TypeDescription
document.createdDocument uploaded
document.sentDocument sent to signers
document.signedIndividual signer completed
document.completedAll signers finished

Note: Additional events (document.viewed, document.declined) are in development and will be available soon.


Security & Signature Verification

All webhook requests include an X-Webhook-Signature header containing an HMAC-SHA256 signature.⚠️ Always verify this signature to ensure the request is authentic.

Signature Headers

X-Webhook-Signature: <hmac-sha256-hex>
X-Webhook-Timestamp: <unix-timestamp-ms>
X-Webhook-Event: <event-type>

Verification Examples

Node.js / Express

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

const app = express();

// IMPORTANT: Use express.raw() to preserve the raw body for signature verification
app.post('/webhooks/esign', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const event = req.headers['x-webhook-event'];

  // Get the raw body as a string
  const payload = req.body.toString('utf8');

  // Verify the signature
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  // Use timing-safe comparison
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Optional: Check timestamp to prevent replay attacks (within 5 minutes)
  const now = Date.now();
  const webhookTime = parseInt(timestamp);
  if (Math.abs(now - webhookTime) > 5 * 60 * 1000) {
    return res.status(401).json({ error: 'Timestamp too old' });
  }

  // Parse and process the webhook
  const webhookData = JSON.parse(payload);
  console.log(`Received ${event} for document ${webhookData.data.documentId}`);

  // Process the webhook asynchronously
  processWebhook(webhookData).catch(err => {
    console.error('Error processing webhook:', err);
  });

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

async function processWebhook(data) {
  switch (data.event) {
    case 'document.sent':
      // Update your database
      await updateDocumentStatus(data.data.documentId, 'sent');
      break;
    case 'document.signed':
      // Mark signer as completed
      await markSignerCompleted(data.data);
      break;
    case 'document.completed':
      // Trigger next steps in your workflow
      await handleDocumentCompletion(data.data);
      break;
  }
}

Python / Flask

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

app = Flask(__name__)

WEBHOOK_SECRET = 'your-webhook-secret'

@app.route('/webhooks/esign', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    event = request.headers.get('X-Webhook-Event')

    # Get raw payload
    payload = request.get_data(as_text=True)

    # Verify signature
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_signature):
        return jsonify({'error': 'Invalid signature'}), 401

    # Optional: Check timestamp (within 5 minutes)
    now = time.time() * 1000
    webhook_time = int(timestamp)
    if abs(now - webhook_time) > 5 * 60 * 1000:
        return jsonify({'error': 'Timestamp too old'}), 401

    # Parse and process webhook
    data = request.get_json()
    print(f"Received {event} for document {data['data']['documentId']}")

    # Process asynchronously
    process_webhook(data)

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

PHP

<?php

function verifyWebhookSignature($payload, $signature, $secret) {
    $expectedSignature = hash_hmac('sha256', $payload, $secret);
    return hash_equals($signature, $expectedSignature);
}

// Get raw POST data
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$event = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';

$secret = getenv('WEBHOOK_SECRET');

// Verify signature
if (!verifyWebhookSignature($payload, $signature, $secret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Parse webhook data
$data = json_decode($payload, true);
error_log("Received {$event} for document {$data['data']['documentId']}");

// Process webhook
processWebhook($data);

// Return success
http_response_code(200);
echo json_encode(['received' => true]);
?>

Payload Structure

All webhook payloads follow this standard structure:

{
  event: string;          // Event type (e.g., 'document.sent')
  timestamp: string;      // ISO 8601 timestamp when event occurred
  data: {
    documentId: string;   // UUID of the document
    title: string;        // Document title
    status: string;       // Current document status
    documentType: string; // Type of document (e.g., 'contract')
    isPrepared: boolean;  // Whether document has fields prepared
    signerCount: number;  // Total number of signers
    createdAt: string;    // When document was created (ISO 8601)
    sentAt?: string;      // When document was sent (ISO 8601)
    completedAt?: string; // When all signers completed (ISO 8601)
    signers?: Array<{     // Optional array of signer details
      email: string;
      name: string;
      status: string;     // 'pending', 'sent', 'viewed', 'signed', 'declined'
      signOrder: number;
      signedAt?: string;  // When this signer completed (ISO 8601)
    }>;
  };
}

Retry Logic

The system automatically retries failed webhook deliveries to ensure reliability.

Retry Schedule

AttemptDelay
1Immediate
21 minute
35 minutes
415 minutes
51 hour
66 hours

Success Criteria

A webhook delivery is considered successful if:

  • Your endpoint returns HTTP status 200-299
  • Response is received within 10 seconds

Best Practices

DO

  • Verify Signatures: Always verify the X-Webhook-Signature header
  • Respond Quickly: Return 200 status within 5 seconds
  • Process Asynchronously: Queue webhook processing for later
  • Implement Idempotency: Handle duplicate webhooks gracefully
  • Log Everything: Keep logs of all webhook events for debugging
  • Use HTTPS: Always use HTTPS endpoints in production
  • Monitor: Set up alerts for failed webhook deliveries
  • Test Thoroughly: Use the Test Webhook button before going live

DON'T

  • Don't Skip Verification: Never trust webhooks without signature verification
  • Don't Block: Don't perform long-running operations in webhook handler
  • Don't Return Errors: Don't return 2xx if processing actually failed
  • Don't Expose Secrets: Keep webhook secrets secure, never commit to git
  • Don't Ignore Timestamps: Check timestamp to prevent replay attacks

Troubleshooting

1. Webhooks Not Received

Possible causes:

  • Webhook URL is incorrect
  • Endpoint is not accessible from the internet
  • Firewall blocking incoming requests
  • Server is down or timing out

Solutions:

  • Use the Test Webhook button to check connectivity
  • Ensure your endpoint is publicly accessible
  • Check firewall rules
  • Verify server logs for incoming requests

2. Signature Verification Fails

Possible causes:

  • Incorrect webhook secret
  • Body was modified before verification
  • Using JSON-parsed body instead of raw body

Solutions:

  • Copy the correct webhook secret from settings
  • Use raw body for signature verification (not parsed JSON)
  • Check your framework's middleware order

Testing Locally

Use ngrok or similar tools to expose local endpoint:

# Install ngrok
npm install -g ngrok

# Expose local server
ngrok http 3000

# Use the generated URL in webhook settings
# https://abc123.ngrok.io/webhooks/esign

Viewing Webhook Logs

  1. Go to Settings → API Keys
  2. Click on your API key
  3. Expand Webhook Configuration
  4. Click View Webhook Logs

Quick Reference

Webhook Headers:

X-Webhook-Signature: <hmac-sha256-hex>
X-Webhook-Timestamp: <unix-timestamp-ms>
X-Webhook-Event: <event-type>

Event Types:

  • document.created- Document uploaded
  • document.sent- Sent to signers
  • document.signed- Signer completed
  • document.completed- All signers done

Retry Schedule:

1min → 5min → 15min → 1hr → 6hr (5 attempts max)