home / skills / jeremylongshore / claude-code-plugins-plus-skills / documenso-webhooks-events
/plugins/saas-packs/documenso-pack/skills/documenso-webhooks-events
This skill guides you to configure and handle Documenso webhooks and events for real-time document signing notifications.
npx playbooks add skill jeremylongshore/claude-code-plugins-plus-skills --skill documenso-webhooks-eventsReview the files below or copy the command above to add this skill to your agents.
---
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 <[email protected]>
---
# Documenso Webhooks & Events
## Overview
Configure and handle Documenso webhooks for real-time document signing notifications.
## Prerequisites
- Documenso team account (webhooks require teams)
- HTTPS endpoint for webhook reception
- Understanding of webhook security
## Supported Events
| Event | Trigger | Description |
|-------|---------|-------------|
| `document.created` | Document created | New document added to system |
| `document.sent` | Document sent | Document sent to recipients |
| `document.opened` | Document opened | Recipient opened document |
| `document.signed` | Recipient signed | One recipient completed signing |
| `document.completed` | All signed | All recipients have signed |
| `document.rejected` | Document rejected | Recipient rejected document |
| `document.cancelled` | Document cancelled | Document was cancelled |
## Webhook Setup
### Step 1: Create Webhook in Dashboard
1. Log into Documenso dashboard
2. Click avatar -> "Team settings"
3. Navigate to "Webhooks" tab
4. Click "Create Webhook"
5. Configure:
- **URL**: Your HTTPS endpoint
- **Events**: Select events to subscribe
- **Secret**: Optional but recommended
### Step 2: Implement Webhook Endpoint
```typescript
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: Use raw body parser for signature verification
app.use("/webhooks/documenso", express.raw({ type: "application/json" }));
interface DocumensoWebhookPayload {
event:
| "document.created"
| "document.sent"
| "document.opened"
| "document.signed"
| "document.completed"
| "document.rejected"
| "document.cancelled";
payload: {
id: string;
title: string;
status: string;
createdAt: string;
updatedAt: string;
documentDataId: string;
userId: string;
teamId?: string;
recipients: Array<{
id: string;
email: string;
name: string;
role: string;
signingStatus: string;
signedAt?: string;
}>;
};
createdAt: string;
webhookEndpoint: string;
}
app.post("/webhooks/documenso", async (req, res) => {
// Step 1: Verify webhook secret
const receivedSecret = req.headers["x-documenso-secret"] as string;
const expectedSecret = process.env.DOCUMENSO_WEBHOOK_SECRET;
if (expectedSecret && receivedSecret !== expectedSecret) {
console.warn("Invalid webhook secret");
return res.status(401).json({ error: "Invalid signature" });
}
// Step 2: Parse payload
let payload: DocumensoWebhookPayload;
try {
payload = JSON.parse(req.body.toString());
} catch (error) {
console.error("Invalid JSON payload");
return res.status(400).json({ error: "Invalid JSON" });
}
// Step 3: Log event
console.log(`Webhook received: ${payload.event}`);
console.log(`Document: ${payload.payload.id} - ${payload.payload.title}`);
// Step 4: Handle event
try {
await handleWebhookEvent(payload);
res.status(200).json({ received: true });
} catch (error) {
console.error("Webhook handling error:", error);
res.status(500).json({ error: "Internal error" });
}
});
async function handleWebhookEvent(
webhook: DocumensoWebhookPayload
): Promise<void> {
const { event, payload } = webhook;
switch (event) {
case "document.created":
await onDocumentCreated(payload);
break;
case "document.sent":
await onDocumentSent(payload);
break;
case "document.opened":
await onDocumentOpened(payload);
break;
case "document.signed":
await onDocumentSigned(payload);
break;
case "document.completed":
await onDocumentCompleted(payload);
break;
case "document.rejected":
await onDocumentRejected(payload);
break;
case "document.cancelled":
await onDocumentCancelled(payload);
break;
default:
console.log(`Unhandled event: ${event}`);
}
}
```
### Step 3: Event Handlers
```typescript
async function onDocumentCreated(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
console.log(`Document created: ${doc.title}`);
// Track in your system
await db.documents.create({
externalId: doc.id,
title: doc.title,
status: "created",
createdAt: new Date(doc.createdAt),
});
}
async function onDocumentSent(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
console.log(`Document sent: ${doc.title}`);
// Update status
await db.documents.update({
where: { externalId: doc.id },
data: { status: "sent", sentAt: new Date() },
});
// Notify internal users
await notifications.send({
channel: "slack",
message: `Document "${doc.title}" sent to ${doc.recipients.length} recipients`,
});
}
async function onDocumentOpened(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
// Find who opened it
const opener = doc.recipients.find(
(r) => r.signingStatus === "NOT_SIGNED" // They opened but haven't signed
);
console.log(`Document opened by: ${opener?.email}`);
// Track for analytics
await analytics.track("document_opened", {
documentId: doc.id,
recipientEmail: opener?.email,
});
}
async function onDocumentSigned(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
// Find who signed
const signer = doc.recipients.find((r) => r.signingStatus === "SIGNED");
console.log(`Document signed by: ${signer?.email}`);
// Update tracking
await db.signatures.create({
documentId: doc.id,
recipientEmail: signer?.email,
signedAt: signer?.signedAt ? new Date(signer.signedAt) : new Date(),
});
// Check if all have signed
const allSigned = doc.recipients.every(
(r) => r.role === "CC" || r.signingStatus === "SIGNED"
);
if (allSigned) {
console.log("All recipients have signed!");
}
}
async function onDocumentCompleted(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
console.log(`Document completed: ${doc.title}`);
// Update status
await db.documents.update({
where: { externalId: doc.id },
data: { status: "completed", completedAt: new Date() },
});
// Download signed document
const client = getDocumensoClient();
const signedDoc = await client.documents.downloadV0({
documentId: doc.id,
});
// Store signed copy
await storage.upload(`signed/${doc.id}.pdf`, signedDoc);
// Trigger downstream processes
await workflows.trigger("document_completed", {
documentId: doc.id,
title: doc.title,
});
}
async function onDocumentRejected(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
const rejecter = doc.recipients.find((r) => r.signingStatus === "REJECTED");
console.log(`Document rejected by: ${rejecter?.email}`);
// Update status
await db.documents.update({
where: { externalId: doc.id },
data: { status: "rejected", rejectedBy: rejecter?.email },
});
// Alert team
await notifications.send({
channel: "slack",
priority: "high",
message: `Document "${doc.title}" rejected by ${rejecter?.email}`,
});
}
async function onDocumentCancelled(
doc: DocumensoWebhookPayload["payload"]
): Promise<void> {
console.log(`Document cancelled: ${doc.title}`);
// Update status
await db.documents.update({
where: { externalId: doc.id },
data: { status: "cancelled", cancelledAt: new Date() },
});
}
```
### Step 4: Idempotency
```typescript
// Prevent duplicate processing
const processedWebhooks = new Set<string>();
async function handleWebhookIdempotent(
webhook: DocumensoWebhookPayload
): Promise<boolean> {
// Create unique key from event + document + timestamp
const key = `${webhook.event}:${webhook.payload.id}:${webhook.createdAt}`;
// Check if already processed
if (processedWebhooks.has(key)) {
console.log(`Duplicate webhook ignored: ${key}`);
return false;
}
// Mark as processed
processedWebhooks.add(key);
// Cleanup old entries (keep last 10000)
if (processedWebhooks.size > 10000) {
const oldest = processedWebhooks.values().next().value;
processedWebhooks.delete(oldest);
}
// Process webhook
await handleWebhookEvent(webhook);
return true;
}
// For production, use Redis or database
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function handleWebhookIdempotentRedis(
webhook: DocumensoWebhookPayload
): Promise<boolean> {
const key = `webhook:${webhook.event}:${webhook.payload.id}:${webhook.createdAt}`;
// Try to set key with 7 day expiry
const result = await redis.set(key, "1", "EX", 604800, "NX");
if (!result) {
console.log(`Duplicate webhook: ${key}`);
return false;
}
await handleWebhookEvent(webhook);
return true;
}
```
## Local Development
```bash
# Start ngrok tunnel
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
# Configure this URL in Documenso webhook settings
# Your endpoint: https://abc123.ngrok.io/webhooks/documenso
```
## Testing Webhooks
```bash
# Test webhook endpoint locally
curl -X POST http://localhost:3000/webhooks/documenso \
-H "Content-Type: application/json" \
-H "X-Documenso-Secret: your-secret" \
-d '{
"event": "document.completed",
"payload": {
"id": "doc_test123",
"title": "Test Document",
"status": "COMPLETED",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T01:00:00Z",
"recipients": [
{
"id": "rec_123",
"email": "[email protected]",
"name": "Test Signer",
"role": "SIGNER",
"signingStatus": "SIGNED"
}
]
},
"createdAt": "2024-01-01T01:00:00Z",
"webhookEndpoint": "https://yourapp.com/webhooks/documenso"
}'
```
## Output
- Webhook endpoint configured
- All events handled
- Idempotency implemented
- Local testing ready
## Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| 401 Unauthorized | Wrong secret | Check webhook secret |
| Webhook not received | URL not HTTPS | Use HTTPS endpoint |
| Duplicate processing | No idempotency | Add deduplication |
| Timeout | Slow handler | Use async queue |
## Resources
- [Documenso Webhooks](https://docs.documenso.com/developers/webhooks)
- [ngrok Documentation](https://ngrok.com/docs)
- [Webhook Best Practices](https://webhooks.fyi/)
## Next Steps
For performance optimization, see `documenso-performance-tuning`.
This skill implements Documenso webhook configuration and event handling for real-time document signing workflows. It provides a secure webhook endpoint pattern, per-event handlers, idempotency strategies, and local testing guidance. Use it to receive and process document lifecycle events reliably and trigger downstream automation.
The skill shows how to register a webhook in the Documenso team settings and implement an HTTPS endpoint that verifies a webhook secret and parses raw JSON payloads. It maps supported events (created, sent, opened, signed, completed, rejected, cancelled) to dedicated handlers that update databases, notify teams, download signed assets, and trigger workflows. It also includes idempotency patterns using in-memory sets or Redis and local testing instructions with ngrok and curl.
How do I verify Documenso webhooks?
Compare the X-Documenso-Secret header with your stored secret. Use raw request body parsing before JSON parsing so verification matches the original payload.
What if my webhook handler is slow and times out?
Acknowledge the webhook quickly (200) and enqueue heavy work for background workers. Use retries and idempotent processing for the queued jobs.
How do I prevent processing the same event twice?
Create a unique key (event:documentId:createdAt) and store it with an expiry in Redis or a database using a set-if-not-exists operation to ensure single processing.