mirror of
https://github.com/documenso/documenso.git
synced 2026-07-15 07:17:09 +10:00
feat: docs v2 (#2460)
Co-authored-by: Catalin Pit <catalinpit@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Create a new webhook
|
||||
|
||||
Click the **Create Webhook** button to open the configuration dialog.
|
||||
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
</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
|
||||
Reference in New Issue
Block a user