Documenso Webhooks & Events
Overview
Configure and handle Documenso webhooks for real-time document lifecycle notifications. Webhooks require a Teams plan or higher. The webhook secret is sent via the X-Documenso-Secret header (not HMAC-signed -- it is a shared secret comparison).
Prerequisites
- Documenso team account (webhooks require teams)
- HTTPS endpoint for webhook reception
- Completed
documenso-install-authsetup
Supported Events
| Event | Trigger | Use Case |
|-------|---------|----------|
| document.created | New document created | Audit logging |
| document.sent | Document sent for signing | Start SLA timers |
| document.opened | Recipient opens the document | Track engagement |
| document.signed | One recipient completes signing | Progress tracking |
| document.completed | All recipients have signed | Trigger downstream workflows |
| document.rejected | Recipient rejects | Alert sender, escalate |
| document.cancelled | Sender cancels document | Cleanup, notify recipients |
Instructions
Step 1: Create Webhook via Dashboard
- Log into Documenso, navigate to Team Settings > Webhooks.
- Click Create Webhook.
- Enter your HTTPS endpoint URL.
- Select the events you want to receive.
- (Optional) Enter a webhook secret -- this value will be sent as-is in the
X-Documenso-Secretheader on every request. - Save.
Step 2: Webhook Handler (Express)
// src/webhooks/documenso.ts
import express from "express";
const router = express.Router();
const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET!;
// Middleware: verify the shared secret
function verifySecret(req: express.Request, res: express.Response, next: express.NextFunction) {
const secret = req.headers["x-documenso-secret"];
if (!secret || secret !== WEBHOOK_SECRET) {
console.warn("Webhook rejected: invalid secret");
return res.status(401).json({ error: "Invalid webhook secret" });
}
next();
}
router.post("/webhooks/documenso", express.json(), verifySecret, async (req, res) => {
const { event, payload } = req.body;
console.log(`Received ${event} for document ${payload.id}`);
// Acknowledge immediately -- process async
res.status(200).json({ received: true });
// Route to handler
try {
await handleEvent(event, payload);
} catch (err) {
console.error(`Failed to process ${event}:`, err);
}
});
async function handleEvent(event: string, payload: any) {
switch (event) {
case "document.completed":
// All recipients signed -- download final PDF, update CRM
await onDocumentCompleted(payload);
break;
case "document.signed":
// One recipient signed -- track progress
await onRecipientSigned(payload);
break;
case "document.rejected":
// Recipient rejected -- alert sender
await onDocumentRejected(payload);
break;
case "document.opened":
// Track engagement for SLA
console.log(`Document ${payload.id} opened by recipient`);
break;
default:
console.log(`Unhandled event: ${event}`);
}
}
async function onDocumentCompleted(payload: any) {
const { id, title, recipients } = payload;
console.log(`Document "${title}" (${id}) completed by all ${recipients?.length} recipients`);
// Download signed PDF, store in S3, update database, notify team
}
async function onRecipientSigned(payload: any) {
console.log(`Recipient signed document ${payload.id}`);
// Update progress tracker, send notification
}
async function onDocumentRejected(payload: any) {
console.log(`Document ${payload.id} REJECTED`);
// Alert sender, create follow-up task
}
export default router;
Step 3: Verification in Python
# webhooks/documenso.py
from flask import Flask, request, jsonify
import hmac
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["DOCUMENSO_WEBHOOK_SECRET"]
@app.route("/webhooks/documenso", methods=["POST"])
def handle_webhook():
# Verify shared secret (constant-time comparison)
secret = request.headers.get("X-Documenso-Secret", "")
if not hmac.compare_digest(secret, WEBHOOK_SECRET):
return jsonify({"error": "Unauthorized"}), 401
data = request.json
event = data["event"]
payload = data["payload"]
print(f"Event: {event}, Document: {payload['id']}")
if event == "document.completed":
# Trigger post-signing workflow
pass
elif event == "document.rejected":
# Alert and escalate
pass
return jsonify({"received": True}), 200
Step 4: Local Development with ngrok
# Start your webhook server
npm run dev # listening on port 3000
# Expose via ngrok
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
# Add as webhook URL in Documenso dashboard:
# https://abc123.ngrok.io/webhooks/documenso
Step 5: Test with curl
# Simulate a webhook delivery locally
curl -X POST http://localhost:3000/webhooks/documenso \
-H "Content-Type: application/json" \
-H "X-Documenso-Secret: $DOCUMENSO_WEBHOOK_SECRET" \
-d '{
"event": "document.completed",
"payload": {
"id": 42,
"title": "Service Agreement",
"status": "COMPLETED",
"recipients": [
{ "email": "signer@example.com", "name": "Jane Doe", "role": "SIGNER" }
]
}
}'
Step 6: Idempotency and Reliable Processing
// Use a Set or database to deduplicate events
const processedEvents = new Set<string>();
async function handleEventIdempotent(event: string, payload: any) {
const eventKey = `${event}:${payload.id}:${payload.updatedAt}`;
if (processedEvents.has(eventKey)) {
console.log(`Skipping duplicate: ${eventKey}`);
return;
}
processedEvents.add(eventKey);
await handleEvent(event, payload);
}
For production, store processed event IDs in Redis or a database table rather than in-memory.
Webhook Payload Structure
{
"event": "document.completed",
"payload": {
"id": 42,
"externalId": null,
"userId": 1,
"teamId": 5,
"title": "Service Agreement",
"status": "COMPLETED",
"createdAt": "2026-03-22T10:00:00.000Z",
"updatedAt": "2026-03-22T14:30:00.000Z",
"completedAt": "2026-03-22T14:30:00.000Z",
"recipients": [
{
"email": "signer@example.com",
"name": "Jane Doe",
"role": "SIGNER",
"signingStatus": "SIGNED"
}
]
}
}
Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| 401 on webhook | Secret mismatch | Verify X-Documenso-Secret matches your stored secret |
| No events received | URL not HTTPS | Use HTTPS endpoint (ngrok for local dev) |
| Duplicate processing | Retry delivery | Implement idempotency with event key deduplication |
| Handler timeout | Slow processing | Acknowledge 200 immediately, process async via queue |
| Events stop arriving | Webhook disabled | Check webhook status in Team Settings |
Resources
Next Steps
For performance optimization, see documenso-performance-tuning.