documenso-webhooks-events
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDocumenso Webhooks & Events
Documenso Webhooks与事件
Overview
概述
Configure and handle Documenso webhooks for real-time document signing notifications.
配置并处理Documenso webhook,以实现文档签署的实时通知。
Prerequisites
前置条件
- Documenso team account (webhooks require teams)
- HTTPS endpoint for webhook reception
- Understanding of webhook security
- Documenso团队账号(webhook功能仅对团队账号开放)
- 用于接收webhook的HTTPS端点
- 了解webhook安全相关知识
Supported Events
支持的事件
| Event | Trigger | Description |
|---|---|---|
| Document created | New document added to system |
| Document sent | Document sent to recipients |
| Document opened | Recipient opened document |
| Recipient signed | One recipient completed signing |
| All signed | All recipients have signed |
| Document rejected | Recipient rejected document |
| Document cancelled | Document was cancelled |
| 事件 | 触发条件 | 描述 |
|---|---|---|
| 文档创建 | 系统中新增了文档 |
| 文档发送 | 文档已发送给收件人 |
| 文档打开 | 收件人打开了文档 |
| 收件人签署 | 一位收件人完成签署 |
| 全部签署 | 所有收件人都已签署 |
| 文档被拒绝 | 收件人拒绝了文档 |
| 文档被取消 | 文档已被取消 |
Webhook Setup
Webhook设置
Step 1: Create Webhook in Dashboard
步骤1:在控制台中创建Webhook
- Log into Documenso dashboard
- Click avatar -> "Team settings"
- Navigate to "Webhooks" tab
- Click "Create Webhook"
- Configure:
- URL: Your HTTPS endpoint
- Events: Select events to subscribe
- Secret: Optional but recommended
- 登录Documenso控制台
- 点击头像 ->「团队设置」
- 导航至「Webhooks」标签页
- 点击「创建Webhook」
- 配置以下内容:
- URL:你的HTTPS端点
- 事件:选择要订阅的事件
- 密钥:可选但建议设置
Step 2: Implement Webhook Endpoint
步骤2:实现Webhook端点
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}`);
}
}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
步骤3:事件处理器
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() },
});
}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
步骤4:幂等性处理
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;
}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
undefinedbash
undefinedStart ngrok tunnel
Start ngrok tunnel
ngrok http 3000
ngrok http 3000
Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
Configure this URL in Documenso webhook settings
Configure this URL in Documenso webhook settings
Your endpoint: https://abc123.ngrok.io/webhooks/documenso
Your endpoint: https://abc123.ngrok.io/webhooks/documenso
undefinedundefinedTesting Webhooks
Webhook测试
bash
undefinedbash
undefinedTest webhook endpoint locally
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": "signer@example.com", "name": "Test Signer", "role": "SIGNER", "signingStatus": "SIGNED" } ] }, "createdAt": "2024-01-01T01:00:00Z", "webhookEndpoint": "https://yourapp.com/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": "signer@example.com", "name": "Test Signer", "role": "SIGNER", "signingStatus": "SIGNED" } ] }, "createdAt": "2024-01-01T01:00:00Z", "webhookEndpoint": "https://yourapp.com/webhooks/documenso" }'
undefinedcurl -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": "signer@example.com", "name": "Test Signer", "role": "SIGNER", "signingStatus": "SIGNED" } ] }, "createdAt": "2024-01-01T01:00:00Z", "webhookEndpoint": "https://yourapp.com/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": "signer@example.com", "name": "Test Signer", "role": "SIGNER", "signingStatus": "SIGNED" } ] }, "createdAt": "2024-01-01T01:00:00Z", "webhookEndpoint": "https://yourapp.com/webhooks/documenso" }'
undefinedOutput
输出结果
- Webhook endpoint configured
- All events handled
- Idempotency implemented
- Local testing ready
- Webhook端点已配置完成
- 所有事件均已处理
- 实现了幂等性
- 可进行本地测试
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 |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 401 未授权 | 密钥错误 | 检查webhook密钥 |
| 未收到Webhook | URL非HTTPS | 使用HTTPS端点 |
| 重复处理 | 未实现幂等性 | 添加去重逻辑 |
| 超时 | 处理器响应过慢 | 使用异步队列 |
Resources
参考资源
Next Steps
下一步
For performance optimization, see .
documenso-performance-tuning如需性能优化,请查看「documenso-performance-tuning」。