--- name: documenso-webhooks-events description: '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". ' allowed-tools: Read, Write, Edit, Bash(curl:*), Bash(ngrok:*) version: 1.0.0 license: MIT author: Jeremy Longshore tags: - saas - documenso - webhooks compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw --- # 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-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 1. Log into Documenso, navigate to **Team Settings > Webhooks**. 2. Click **Create Webhook**. 3. Enter your **HTTPS endpoint URL**. 4. Select the events you want to receive. 5. (Optional) Enter a **webhook secret** -- this value will be sent as-is in the `X-Documenso-Secret` header on every request. 6. Save. ### Step 2: Webhook Handler (Express) ```typescript // 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 ```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 ```bash # 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 ```bash # 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 ```typescript // Use a Set or database to deduplicate events const processedEvents = new Set(); 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 ```json { "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 - [Documenso Webhooks](https://docs.documenso.com/developers/webhooks) - [Webhook Verification](https://docs.documenso.com/docs/developers/webhooks/verification) - [ngrok Documentation](https://ngrok.com/docs) ## Next Steps For performance optimization, see `documenso-performance-tuning`.