feat: docs v2 (#2460)

Co-authored-by: Catalin Pit <catalinpit@gmail.com>
This commit is contained in:
Lucas Smith
2026-02-27 22:05:27 +11:00
committed by GitHub
parent f8ac782f2e
commit b92c53dbb2
290 changed files with 32521 additions and 266 deletions
@@ -0,0 +1,679 @@
---
title: Webhook Events
description: Reference for all webhook event types and payloads.
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
## Event Payload Structure
All webhook events share a common structure:
```json
{
"event": "DOCUMENT_COMPLETED",
"payload": {
// Document data with recipients
},
"createdAt": "2024-04-22T11:52:18.277Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
### Top-Level Fields
| Field | Type | Description |
| ----------------- | -------- | ------------------------------------------------ |
| `event` | string | Event type identifier (e.g., `DOCUMENT_CREATED`) |
| `payload` | object | Document object with metadata and recipients |
| `createdAt` | datetime | When the webhook event was created |
| `webhookEndpoint` | string | The URL receiving this webhook |
### Payload Fields
| Field | Type | Description |
| ---------------- | --------- | ------------------------------------------------------ |
| `id` | number | Document ID |
| `externalId` | string? | External identifier for integration |
| `userId` | number | Owner's user ID |
| `authOptions` | object? | Document-level authentication options |
| `formValues` | object? | PDF form values associated with the document |
| `title` | string | Document title |
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED` |
| `documentDataId` | string | Reference to the document's PDF data |
| `visibility` | string | Document visibility setting |
| `createdAt` | datetime | Document creation timestamp |
| `updatedAt` | datetime | Last modification timestamp |
| `completedAt` | datetime? | Completion timestamp (when all recipients have signed) |
| `deletedAt` | datetime? | Deletion timestamp |
| `teamId` | number? | Team ID if document belongs to a team |
| `templateId` | number? | Template ID if created from a template |
| `source` | string | Source: `DOCUMENT` or `TEMPLATE` |
| `documentMeta` | object | Document metadata (subject, message, signing options) |
| `Recipient` | array | List of recipient objects |
### Document Metadata Fields
| Field | Type | Description |
| ----------------------- | ------- | --------------------------------------- |
| `id` | string | Metadata record identifier |
| `subject` | string? | Email subject line |
| `message` | string? | Email message body |
| `timezone` | string | Timezone for date display |
| `password` | string? | Document access password (if set) |
| `dateFormat` | string | Date format string |
| `redirectUrl` | string? | URL to redirect after signing |
| `signingOrder` | string | `PARALLEL` or `SEQUENTIAL` |
| `typedSignatureEnabled` | boolean | Whether typed signatures are allowed |
| `language` | string | Document language code |
| `distributionMethod` | string | How document is distributed |
| `emailSettings` | object? | Custom email settings for this document |
### Recipient Fields
| Field | Type | Description |
| ------------------- | --------- | ------------------------------------------ |
| `id` | number | Recipient ID |
| `documentId` | number | Parent document ID |
| `templateId` | number? | Template ID if created from a template |
| `email` | string | Recipient email address |
| `name` | string | Recipient name |
| `token` | string | Unique signing token |
| `documentDeletedAt` | datetime? | When the document was deleted (if deleted) |
| `expired` | boolean? | Whether the recipient's link has expired |
| `signedAt` | datetime? | When recipient signed |
| `authOptions` | object? | Per-recipient authentication options |
| `role` | string | Role: `SIGNER`, `VIEWER`, `APPROVER`, `CC` |
| `signingOrder` | number? | Position in signing sequence |
| `readStatus` | string | `NOT_OPENED` or `OPENED` |
| `signingStatus` | string | `NOT_SIGNED`, `SIGNED`, or `REJECTED` |
| `sendStatus` | string | `NOT_SENT` or `SENT` |
| `rejectionReason` | string? | Reason if recipient rejected |
---
## Document Lifecycle Events
These events track the document through its lifecycle.
### `document.created`
Triggered when a new document is uploaded.
**Event name:** `DOCUMENT_CREATED`
```json
{
"event": "DOCUMENT_CREATED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:44:43.341Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T11:44:44.779Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
### `document.sent`
Triggered when a document is sent to recipients for signing.
**Event name:** `DOCUMENT_SENT`
The document status changes to `PENDING` and recipients have `sendStatus: "SENT"`.
```json
{
"event": "DOCUMENT_SENT",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:48:07.945Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
### `document.completed`
Triggered when all recipients have completed their required actions.
**Event name:** `DOCUMENT_COMPLETED`
The document status changes to `COMPLETED` and `completedAt` is set.
```json
{
"event": "DOCUMENT_COMPLETED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "COMPLETED",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:52:05.708Z",
"completedAt": "2024-04-22T11:52:05.707Z",
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 50,
"documentId": 10,
"templateId": null,
"email": "reviewer@example.com",
"name": "Jane Smith",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:51:10.055Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "VIEWER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
},
{
"id": 51,
"documentId": 10,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "HkrptwS42ZBXdRKj1TyUo",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:52:05.688Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 2,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:52:18.277Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
### `document.rejected`
Triggered when a recipient rejects the document.
**Event name:** `DOCUMENT_REJECTED`
The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` contains their reason.
```json
{
"event": "DOCUMENT_REJECTED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:48:07.569Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": "I do not agree with the terms",
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "REJECTED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:48:07.945Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
### `document.cancelled`
Triggered when the document owner cancels a pending document.
**Event name:** `DOCUMENT_CANCELLED`
```json
{
"event": "DOCUMENT_CANCELLED",
"payload": {
"id": 7,
"externalId": null,
"userId": 3,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "PENDING",
"documentDataId": "cm6exvn93006hi02ru90a265a",
"createdAt": "2025-01-27T11:02:14.393Z",
"updatedAt": "2025-01-27T11:03:16.387Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "cm6exvn96006ji02rqvzjvwoy",
"subject": "",
"message": "",
"timezone": "Etc/UTC",
"password": null,
"dateFormat": "yyyy-MM-dd hh:mm a",
"redirectUrl": "",
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 7,
"documentId": 7,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "XkKx1HCs6Znm2UBJA2j6o",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": { "accessAuth": null, "actionAuth": null },
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2025-01-27T11:03:27.730Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
---
## Recipient Events
Recipient events track individual signer actions. These events use the same payload structure as document events, but focus on a specific recipient's action.
### `document.opened`
Triggered when a recipient opens the document for the first time.
**Event name:** `DOCUMENT_OPENED`
The recipient's `readStatus` changes to `OPENED`.
```json
{
"event": "DOCUMENT_OPENED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:50:26.174Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
### `document.signed`
Triggered when a recipient signs the document.
**Event name:** `DOCUMENT_SIGNED`
The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
```json
{
"event": "DOCUMENT_SIGNED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "contract.pdf",
"status": "COMPLETED",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:52:05.708Z",
"completedAt": "2024-04-22T11:52:05.707Z",
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 51,
"documentId": 10,
"templateId": null,
"email": "signer@example.com",
"name": "John Doe",
"token": "HkrptwS42ZBXdRKj1TyUo",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:52:05.688Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:52:18.577Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
---
## Event Summary
| Event | Trigger | Key Changes |
| -------------------- | ------------------------------- | ------------------------------------------------------------ |
| `DOCUMENT_CREATED` | Document uploaded | `status: "DRAFT"` |
| `DOCUMENT_SENT` | Document sent to recipients | `status: "PENDING"`, recipients `sendStatus: "SENT"` |
| `DOCUMENT_OPENED` | Recipient opens document | Recipient `readStatus: "OPENED"` |
| `DOCUMENT_SIGNED` | Recipient signs document | Recipient `signingStatus: "SIGNED"`, `signedAt` set |
| `DOCUMENT_COMPLETED` | All recipients complete actions | `status: "COMPLETED"`, `completedAt` set |
| `DOCUMENT_REJECTED` | Recipient rejects document | Recipient `signingStatus: "REJECTED"`, `rejectionReason` set |
| `DOCUMENT_CANCELLED` | Owner cancels document | Document cancelled while pending |
---
## Handling Events
When processing webhook events:
{/* prettier-ignore */}
<Steps>
<Step>
**Verify the signature** — Check the `X-Documenso-Secret` header matches your configured secret
</Step>
<Step>
**Check event type** — Use the `event` field to determine the action
</Step>
<Step>
**Process idempotently** — Webhooks may be retried, so handle duplicate events
</Step>
<Step>
**Respond quickly** — Return a 200 status code within 30 seconds
</Step>
</Steps>
```typescript
app.post('/webhook', (req, res) => {
const secret = req.headers['x-documenso-secret'];
if (secret !== process.env.WEBHOOK_SECRET) {
return res.status(401).send('Unauthorized');
}
const { event, payload } = req.body;
switch (event) {
case 'DOCUMENT_COMPLETED':
// Handle completed document
console.log(`Document ${payload.id} completed`);
break;
case 'DOCUMENT_SIGNED':
// Handle signature
const signer = payload.Recipient.find((r) => r.signingStatus === 'SIGNED');
console.log(`${signer?.name} signed document ${payload.id}`);
break;
case 'DOCUMENT_REJECTED':
// Handle rejection
const rejecter = payload.Recipient.find((r) => r.signingStatus === 'REJECTED');
console.log(`${rejecter?.name} rejected: ${rejecter?.rejectionReason}`);
break;
}
res.status(200).send('OK');
});
```
---
## See Also
- [Webhook Setup](/docs/developers/webhooks/setup) - Configure webhook endpoints
- [Webhook Verification](/docs/developers/webhooks/verification) - Verify webhook signatures
@@ -0,0 +1,55 @@
---
title: Webhooks
description: Receive real-time notifications when documents are signed, completed, or updated.
---
## How Webhooks Work
1. You configure a webhook URL in Documenso
2. When an event occurs, Documenso sends an HTTP POST to your URL
3. Your application processes the event and responds with 200 OK
---
## Getting Started
<Cards>
<Card
title="Setup"
description="Configure webhook endpoints."
href="/docs/developers/webhooks/setup"
/>
<Card
title="Events"
description="Available webhook event types."
href="/docs/developers/webhooks/events"
/>
<Card
title="Verification"
description="Verify webhook signatures for security."
href="/docs/developers/webhooks/verification"
/>
</Cards>
---
## Example Payload
```json
{
"event": "DOCUMENT_COMPLETED",
"payload": {
"id": 123,
"title": "Contract",
"status": "COMPLETED"
},
"createdAt": "2024-01-15T10:30:00.000Z",
"webhookEndpoint": "https://your-endpoint.com/webhook"
}
```
---
## See Also
- [Document Lifecycle](/docs/concepts/document-lifecycle) - Understanding document statuses
@@ -0,0 +1,4 @@
{
"title": "Webhooks",
"pages": ["setup", "events", "verification"]
}
@@ -0,0 +1,355 @@
---
title: Webhook Setup
description: Configure webhooks to receive real-time notifications about document events.
---
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Overview
Webhooks are HTTP callbacks triggered by specific events in Documenso. When an event occurs (such as a document being signed), Documenso sends an HTTP POST request to your configured URL with details about the event.
Common use cases include:
- Syncing document status with your database
- Triggering automated workflows when documents are signed
- Integrating with CRM systems or other third-party services
- Sending custom notifications to stakeholders
<Callout type="info">
Webhooks are available for teams only. Personal accounts cannot configure webhooks.
</Callout>
## Creating a Webhook Endpoint
Before configuring a webhook in Documenso, you need an endpoint that can receive HTTP POST requests. Here's a minimal example:
<Tabs items={['Node.js (Express)', 'Python (Flask)', 'Go']}>
<Tab value="Node.js (Express)">
```javascript
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/documenso', (req, res) => {
const { event, payload, createdAt } = req.body;
console.log(`Received event: ${event}`);
console.log(`Document ID: ${payload.id}`);
console.log(`Document title: ${payload.title}`);
// Process the webhook event
switch (event) {
case 'DOCUMENT_COMPLETED':
// Handle completed document
break;
case 'DOCUMENT_SIGNED':
// Handle signed document
break;
// Handle other events...
}
// Respond with 200 OK to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
````
</Tab>
<Tab value="Python (Flask)">
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/documenso', methods=['POST'])
def handle_webhook():
data = request.get_json()
event = data.get('event')
payload = data.get('payload')
print(f"Received event: {event}")
print(f"Document ID: {payload.get('id')}")
print(f"Document title: {payload.get('title')}")
# Process the webhook event
if event == 'DOCUMENT_COMPLETED':
# Handle completed document
pass
elif event == 'DOCUMENT_SIGNED':
# Handle signed document
pass
# Respond with 200 OK to acknowledge receipt
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
````
</Tab>
<Tab value="Go">
```go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type WebhookPayload struct {
Event string `json:"event"`
Payload map[string]interface{} `json:"payload"`
CreatedAt string `json:"createdAt"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var data WebhookPayload
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
fmt.Printf("Received event: %s\n", data.Event)
fmt.Printf("Document ID: %v\n", data.Payload["id"])
fmt.Printf("Document title: %v\n", data.Payload["title"])
// Process the webhook event
switch data.Event {
case "DOCUMENT_COMPLETED":
// Handle completed document
case "DOCUMENT_SIGNED":
// Handle signed document
}
// Respond with 200 OK to acknowledge receipt
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func main() {
http.HandleFunc("/webhooks/documenso", webhookHandler)
fmt.Println("Webhook server running on port 3000")
http.ListenAndServe(":3000", nil)
}
```
</Tab>
</Tabs>
<Callout type="warn">
Always respond with a `200 OK` status within 30 seconds. Documenso will retry failed deliveries.
</Callout>
## Configuring Webhooks in Documenso via the Dashboard
{/* prettier-ignore */}
<Steps>
<Step>
### Navigate to team settings
Click your avatar in the top right corner and select **Team settings** from the dropdown menu.
</Step>
<Step>
### Open the webhooks tab
Navigate to the **Webhooks** tab in the team settings sidebar.
![Webhooks settings page](/webhook-images/webhooks-page.webp)
</Step>
<Step>
### Create a new webhook
Click the **Create Webhook** button to open the configuration dialog.
![Create webhook dialog](/webhook-images/create-webhook-dialog.webp)
</Step>
<Step>
### Configure the webhook
Fill in the following fields:
| Field | Description |
| ----- | ----------- |
| **Webhook URL** | The HTTPS endpoint that will receive webhook events |
| **Events** | Select which events should trigger this webhook |
| **Secret** (optional) | A secret key used to sign the payload for verification |
</Step>
<Step>
### Save the webhook
Click **Create Webhook** to save your configuration. The webhook is now active and will receive events.
</Step>
</Steps>
## Webhook URL Requirements
Your webhook endpoint must meet these requirements:
| Requirement | Details |
| ----------- | ------- |
| **Protocol** | HTTPS required (HTTP not allowed in production) |
| **Response** | Must return `2xx` status code within 30 seconds |
| **Method** | Must accept HTTP POST requests |
| **Content-Type** | Must accept `application/json` payloads |
| **Availability** | Must be publicly accessible from the internet |
<Callout type="info">
For local development, use a tunneling service like [ngrok](https://ngrok.com) or [localtunnel](https://localtunnel.me) to expose your local server.
</Callout>
## Selecting Events
When creating a webhook, you can subscribe to one or more events:
| Event | Trigger |
| ----- | ------- |
| `DOCUMENT_CREATED` | A new document is created |
| `DOCUMENT_SENT` | A document is sent to recipients |
| `DOCUMENT_OPENED` | A recipient opens the document |
| `DOCUMENT_SIGNED` | A recipient signs the document |
| `DOCUMENT_COMPLETED` | All recipients have signed the document |
| `DOCUMENT_REJECTED` | A recipient rejects the document |
| `DOCUMENT_CANCELLED` | The document owner cancels the document |
You can subscribe to all events or select specific ones based on your needs. For example, if you only need to know when documents are fully signed, subscribe only to `DOCUMENT_COMPLETED`.
See [Webhook Events](/docs/developers/webhooks/events) for detailed payload information for each event type.
## Testing Webhooks
Documenso provides a built-in testing feature to verify your webhook endpoint works correctly.
{/* prettier-ignore */}
<Steps>
<Step>
### Navigate to webhook details
Go to **Team Settings > Webhooks** and click on the webhook you want to test.
![Webhook detail page](/webhook-images/webhook-detail-page.webp)
</Step>
<Step>
### Click test
Click the **Test** button in the webhook details page.
</Step>
<Step>
### Select an event type
Choose which event type you want to simulate from the dropdown.
</Step>
<Step>
### Send test payload
Click **Send** to dispatch a test webhook with sample data to your endpoint.
![Webhook test trigger](/webhook-images/webhook-test-trigger.webp)
</Step>
</Steps>
The test payload contains realistic sample data so you can verify your endpoint processes events correctly. After sending, you can view the response in the webhook call logs.
### Viewing Webhook Logs
Each webhook subscription maintains a log of all delivery attempts. To view logs:
{/* prettier-ignore */}
<Steps>
<Step>
Go to **Team Settings > Webhooks**
</Step>
<Step>
Click on a webhook to view its details
</Step>
<Step>
Review the logs
Each webhook call shows the following details:
- Status (success/failure)
- Event type
- Timestamp
- Response code
- Request and response bodies
Click any call to see full details including headers and response data.
</Step>
</Steps>
### Resending Failed Webhooks
If a webhook delivery fails, you can manually resend it:
{/* prettier-ignore */}
<Steps>
<Step>
Navigate to the webhook call details page
</Step>
<Step>
Click the **Resend** button
</Step>
<Step>
Documenso will attempt to deliver the same payload again
</Step>
</Steps>
## Retry Policy
When a webhook delivery fails (non-2xx response or timeout), Documenso automatically retries with exponential backoff:
| Attempt | Delay |
| ------- | ----- |
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as failed and no further automatic retries occur. You can manually resend failed webhooks from the dashboard.
<Callout type="warn">
If your endpoint consistently fails, consider reviewing your server logs and ensuring your endpoint meets all [URL requirements](#webhook-url-requirements).
</Callout>
## Security Best Practices
<Accordions type="multiple">
<Accordion title="Use HTTPS">
Always use HTTPS endpoints in production.
</Accordion>
<Accordion title="Verify signatures">
Use a webhook secret and verify the `X-Documenso-Secret` header (see [Verification](/docs/developers/webhooks/verification)).
</Accordion>
<Accordion title="Validate payloads">
Validate incoming data before processing.
</Accordion>
<Accordion title="Respond quickly">
Return 200 OK immediately, then process asynchronously.
</Accordion>
<Accordion title="Idempotency">
Handle duplicate deliveries gracefully.
</Accordion>
</Accordions>
## Next Steps
- [Webhook Events](/docs/developers/webhooks/events) - Detailed payload structure for each event type
- [Webhook Verification](/docs/developers/webhooks/verification) - Secure your webhooks with signature verification
```
@@ -0,0 +1,290 @@
---
title: Webhook Verification
description: Verify webhook signatures for security.
---
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Overview
Verifying webhook requests ensures that incoming payloads originate from Documenso and have not been tampered with. Without verification, attackers could forge requests to your endpoint and trigger unintended actions.
## How Documenso Signs Webhooks
When you configure a webhook with a secret, Documenso includes that secret in every webhook request via the `X-Documenso-Secret` header. Your server should compare this header value against your stored secret to authenticate the request.
```bash
POST /your-webhook-endpoint HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-Documenso-Secret: your_webhook_secret_here
{"event": "DOCUMENT_COMPLETED", "payload": {...}}
```
## Signature Header Format
| Header | Description |
| -------------------- | ------------------------------------------------------- |
| `X-Documenso-Secret` | The secret key you configured when creating the webhook |
The header contains your webhook secret as a plain string. If you did not configure a secret, the header will be an empty string.
## Verification Steps
{/* prettier-ignore */}
<Steps>
<Step>
Extract the `X-Documenso-Secret` header from the incoming request
</Step>
<Step>
Compare it against your stored webhook secret using a constant-time comparison
</Step>
<Step>
Reject the request if the values do not match
</Step>
<Step>
Process the webhook payload if verification succeeds
</Step>
</Steps>
<Callout type="warn">
Always use constant-time string comparison to prevent timing attacks. Standard equality operators
(`===` or `==`) can leak information about the secret through response time variations.
</Callout>
## Code Examples
<Tabs items={['Node.js (Express)', 'Python (Flask)']}>
<Tab value="Node.js (Express)">
```javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET;
function verifyWebhookSignature(receivedSecret, expectedSecret) {
if (!expectedSecret) {
// No secret configured, skip verification
// Not recommended for production
return true;
}
if (!receivedSecret) {
return false;
}
// Use constant-time comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(receivedSecret),
Buffer.from(expectedSecret),
);
} catch {
return false;
}
}
app.post('/webhooks/documenso', (req, res) => {
const receivedSecret = req.headers['x-documenso-secret'];
if (!verifyWebhookSignature(receivedSecret, WEBHOOK_SECRET)) {
console.error('Webhook verification failed');
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature verified, process the webhook
const { event, payload } = req.body;
console.log(`Verified webhook: ${event}`);
// Process the event...
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
````
</Tab>
<Tab value="Python (Flask)">
```python
import hmac
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('DOCUMENSO_WEBHOOK_SECRET')
def verify_webhook_signature(received_secret, expected_secret):
"""Verify the webhook signature using constant-time comparison."""
if not expected_secret:
# No secret configured, skip verification
# Not recommended for production
return True
if not received_secret:
return False
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(received_secret, expected_secret)
@app.route('/webhooks/documenso', methods=['POST'])
def handle_webhook():
received_secret = request.headers.get('X-Documenso-Secret', '')
if not verify_webhook_signature(received_secret, WEBHOOK_SECRET):
print('Webhook verification failed')
return jsonify({'error': 'Invalid signature'}), 401
# Signature verified, process the webhook
data = request.get_json()
event = data.get('event')
payload = data.get('payload')
print(f'Verified webhook: {event}')
# Process the event...
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
````
</Tab>
</Tabs>
## Handling Verification Failures
When verification fails, follow these practices:
| Action | Description |
| ------------------- | ------------------------------------------------------------------ |
| Return 401 status | Respond with `401 Unauthorized` to indicate authentication failure |
| Log the attempt | Record failed attempts for security monitoring |
| Do not process | Never process the payload if verification fails |
| Do not leak details | Avoid exposing information about why verification failed |
```javascript
app.post('/webhooks/documenso', (req, res) => {
const receivedSecret = req.headers['x-documenso-secret'];
if (!verifyWebhookSignature(receivedSecret, WEBHOOK_SECRET)) {
// Log for monitoring but don't expose details
console.error('Webhook verification failed', {
timestamp: new Date().toISOString(),
ip: req.ip,
});
// Generic error response
return res.status(401).json({ error: 'Unauthorized' });
}
// Continue processing...
});
```
### Common Verification Issues
| Issue | Cause | Solution |
| --------------- | --------------------------------------- | ------------------------------------------------------------------ |
| Secret mismatch | Webhook secret changed or misconfigured | Verify the secret in your environment matches the one in Documenso |
| Empty header | Webhook created without a secret | Add a secret to the webhook configuration in Documenso |
| Encoding issues | String encoding mismatch | Ensure both secrets use the same encoding (UTF-8) |
## Security Best Practices
<Accordions type="multiple">
<Accordion title="Use strong secrets">
Generate a cryptographically secure random string for your webhook secret:
```bash
# Generate a 32-byte random secret
openssl rand -hex 32
```
</Accordion>
<Accordion title="Store secrets securely">
Never hardcode secrets in your source code. Use environment variables or a secrets manager:
```javascript
// Good: Environment variable
const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET;
// Bad: Hardcoded
const WEBHOOK_SECRET = 'my-secret-key'; // Never do this
```
</Accordion>
<Accordion title="Rotate secrets periodically">
Update your webhook secret periodically:
1. Generate a new secret
2. Update your server to accept both old and new secrets temporarily
3. Update the webhook configuration in Documenso
4. Remove the old secret from your server
</Accordion>
<Accordion title="Validate payload structure">
After verifying the signature, validate the payload structure before processing:
```javascript
const { event, payload, createdAt } = req.body;
if (!event || !payload) {
return res.status(400).json({ error: 'Invalid payload structure' });
}
// Validate event is a known type
const validEvents = [
'DOCUMENT_CREATED',
'DOCUMENT_SENT',
'DOCUMENT_OPENED',
'DOCUMENT_SIGNED',
'DOCUMENT_COMPLETED',
'DOCUMENT_REJECTED',
'DOCUMENT_CANCELLED',
];
if (!validEvents.includes(event)) {
console.warn(`Unknown event type: ${event}`);
}
```
</Accordion>
<Accordion title="Use HTTPS">
Always use HTTPS endpoints in production to encrypt data in transit, including the secret header.
</Accordion>
<Accordion title="Implement rate limiting">
Protect your endpoint from abuse with rate limiting:
```javascript
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: { error: 'Too many requests' },
});
app.post('/webhooks/documenso', webhookLimiter, (req, res) => {
// Handle webhook...
});
```
</Accordion>
</Accordions>
## See Also
- [Webhook Setup](/docs/developers/webhooks/setup) - Configure webhook endpoints and secrets
- [Webhook Events](/docs/developers/webhooks/events) - Event types and payload structure