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.
Table of Contents
Getting Started
1Configure Your Webhook
- Navigate to Settings → API Keys
- Select the API key you want to configure webhooks for
- Expand the Webhook Configuration section
- Enable webhooks with the toggle switch
- Enter your webhook endpoint URL (must be HTTPS in production)
- Select which events you want to subscribe to
- Copy your webhook secret for signature verification
- Click Save Configuration
- 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 Type | Description |
|---|---|
document.created | Document uploaded |
document.sent | Document sent to signers |
document.signed | Individual signer completed |
document.completed | All 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}), 200PHP
<?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
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 6 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-Signatureheader - •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/esignViewing Webhook Logs
- Go to Settings → API Keys
- Click on your API key
- Expand Webhook Configuration
- 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 uploadeddocument.sent- Sent to signersdocument.signed- Signer completeddocument.completed- All signers done
Retry Schedule:
1min → 5min → 15min → 1hr → 6hr (5 attempts max)