From documenso-pack
Sets up Documenso webhook endpoints with secret verification to handle document events like signed, completed, or rejected in Node.js/Express apps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/documenso-pack:documenso-webhooks-eventsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
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).
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).
documenso-install-auth setup| 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 |
X-Documenso-Secret header on every request.// 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;
# 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
# 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
# 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": "[email protected]", "name": "Jane Doe", "role": "SIGNER" }
]
}
}'
// 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.
{
"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": "[email protected]",
"name": "Jane Doe",
"role": "SIGNER",
"signingStatus": "SIGNED"
}
]
}
}
| 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 |
For performance optimization, see documenso-performance-tuning.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin documenso-packManages Documenso document lifecycle, downloads signed PDFs, handles PII sanitization, and implements retention policies in integrations.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.