Implement Documenso webhook configuration and event handling.
Use when setting up webhook endpoints, handling document events,
or implementing real-time notifications for document signing.
Trigger with phrases like "documenso webhook", "documenso events",
"document completed webhook", "signing notification".
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-auth setup
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-Secret header 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;
# 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
// 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.