diff --git a/apps/docs/content/docs/developers/webhooks/events.mdx b/apps/docs/content/docs/developers/webhooks/events.mdx index 038c1d695..9f63f78ac 100644 --- a/apps/docs/content/docs/developers/webhooks/events.mdx +++ b/apps/docs/content/docs/developers/webhooks/events.mdx @@ -13,7 +13,7 @@ All webhook events share a common structure: { "event": "DOCUMENT_COMPLETED", "payload": { - // Document data with recipients + // Document or template data with recipients }, "createdAt": "2024-04-22T11:52:18.277Z", "webhookEndpoint": "https://your-endpoint.com/webhook" @@ -33,14 +33,13 @@ All webhook events share a common structure: | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------ | -| `id` | number | Document ID | +| `id` | number | Document or template 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 | +| `title` | string | Document or template 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 | @@ -50,45 +49,50 @@ All webhook events share a common structure: | `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 | +| `recipients` | array | List of recipient objects | +| `Recipient` | array | List of recipient objects (legacy, same as recipients) | ### 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 | +| 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` | +| `allowDictateNextSigner` | boolean | Whether signers can choose the next signer | +| `typedSignatureEnabled` | boolean | Whether typed signatures are allowed | +| `uploadSignatureEnabled` | boolean | Whether uploaded signatures are allowed | +| `drawSignatureEnabled` | boolean | Whether drawn 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 | +| 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 recipient hid the document | +| `expiresAt` | datetime? | When the recipient's signing link expires | +| `expirationNotifiedAt` | datetime? | When the expiration notification was sent | +| `signedAt` | datetime? | When recipient signed | +| `authOptions` | object? | Per-recipient authentication options | +| `role` | string | Role: `SIGNER`, `VIEWER`, `APPROVER`, `ASSISTANT`, `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 | --- @@ -98,7 +102,7 @@ These events track the document through its lifecycle. ### `document.created` -Triggered when a new document is uploaded. +Triggered when a new document is created. **Event name:** `DOCUMENT_CREATED` @@ -114,7 +118,6 @@ Triggered when a new document is uploaded. "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, @@ -131,11 +134,35 @@ Triggered when a new document is uploaded. "dateFormat": "MM/DD/YYYY", "redirectUrl": null, "signingOrder": "PARALLEL", + "allowDictateNextSigner": false, "typedSignatureEnabled": true, + "uploadSignatureEnabled": true, + "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, + "recipients": [ + { + "id": 52, + "documentId": 10, + "templateId": null, + "email": "signer@example.com", + "name": "John Doe", + "token": "vbT8hi3jKQmrFP_LN1WcS", + "documentDeletedAt": null, + "expiresAt": null, + "expirationNotifiedAt": null, + "signedAt": null, + "authOptions": null, + "signingOrder": 1, + "rejectionReason": null, + "role": "SIGNER", + "readStatus": "NOT_OPENED", + "signingStatus": "NOT_SIGNED", + "sendStatus": "NOT_SENT" + } + ], "Recipient": [ { "id": 52, @@ -145,7 +172,8 @@ Triggered when a new document is uploaded. "name": "John Doe", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, - "expired": null, + "expiresAt": null, + "expirationNotifiedAt": null, "signedAt": null, "authOptions": null, "signingOrder": 1, @@ -182,7 +210,6 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "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, @@ -199,11 +226,35 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "dateFormat": "MM/DD/YYYY", "redirectUrl": null, "signingOrder": "PARALLEL", + "allowDictateNextSigner": false, "typedSignatureEnabled": true, + "uploadSignatureEnabled": true, + "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, + "recipients": [ + { + "id": 52, + "documentId": 10, + "templateId": null, + "email": "signer@example.com", + "name": "John Doe", + "token": "vbT8hi3jKQmrFP_LN1WcS", + "documentDeletedAt": null, + "expiresAt": null, + "expirationNotifiedAt": null, + "signedAt": null, + "authOptions": null, + "signingOrder": 1, + "rejectionReason": null, + "role": "SIGNER", + "readStatus": "NOT_OPENED", + "signingStatus": "NOT_SIGNED", + "sendStatus": "SENT" + } + ], "Recipient": [ { "id": 52, @@ -213,7 +264,8 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "name": "John Doe", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, - "expired": null, + "expiresAt": null, + "expirationNotifiedAt": null, "signedAt": null, "authOptions": null, "signingOrder": 1, @@ -230,6 +282,106 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" } ``` +### `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, + "status": "PENDING", + "title": "contract.pdf", + "source": "DOCUMENT", + "recipients": [ + { + "id": 52, + "email": "signer@example.com", + "name": "John Doe", + "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. This fires for each individual signature, not just when the document is fully completed. + +**Event name:** `DOCUMENT_SIGNED` + +The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated. + +```json +{ + "event": "DOCUMENT_SIGNED", + "payload": { + "id": 10, + "status": "COMPLETED", + "title": "contract.pdf", + "source": "DOCUMENT", + "completedAt": "2024-04-22T11:52:05.707Z", + "recipients": [ + { + "id": 51, + "email": "signer@example.com", + "name": "John Doe", + "role": "SIGNER", + "signedAt": "2024-04-22T11:52:05.688Z", + "readStatus": "OPENED", + "signingStatus": "SIGNED", + "sendStatus": "SENT" + } + ] + }, + "createdAt": "2024-04-22T11:52:18.577Z", + "webhookEndpoint": "https://your-endpoint.com/webhook" +} +``` + +### `document.recipient.completed` + +Triggered when an individual recipient completes their required action (signing, approving, or viewing). This is useful for tracking per-recipient progress in documents with multiple recipients. + +**Event name:** `DOCUMENT_RECIPIENT_COMPLETED` + +```json +{ + "event": "DOCUMENT_RECIPIENT_COMPLETED", + "payload": { + "id": 10, + "status": "PENDING", + "title": "contract.pdf", + "source": "DOCUMENT", + "recipients": [ + { + "id": 52, + "email": "signer@example.com", + "name": "John Doe", + "role": "SIGNER", + "signedAt": "2024-04-22T11:52:05.688Z", + "readStatus": "OPENED", + "signingStatus": "SIGNED", + "sendStatus": "SENT" + } + ] + }, + "createdAt": "2024-04-22T11:52:06.000Z", + "webhookEndpoint": "https://your-endpoint.com/webhook" +} +``` + ### `document.completed` Triggered when all recipients have completed their required actions. @@ -250,7 +402,6 @@ The document status changes to `COMPLETED` and `completedAt` is set. "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", @@ -267,12 +418,15 @@ The document status changes to `COMPLETED` and `completedAt` is set. "dateFormat": "MM/DD/YYYY", "redirectUrl": null, "signingOrder": "PARALLEL", + "allowDictateNextSigner": false, "typedSignatureEnabled": true, + "uploadSignatureEnabled": true, + "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, - "Recipient": [ + "recipients": [ { "id": 50, "documentId": 10, @@ -281,7 +435,8 @@ The document status changes to `COMPLETED` and `completedAt` is set. "name": "Jane Smith", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, - "expired": null, + "expiresAt": null, + "expirationNotifiedAt": null, "signedAt": "2024-04-22T11:51:10.055Z", "authOptions": { "accessAuth": null, @@ -302,7 +457,54 @@ The document status changes to `COMPLETED` and `completedAt` is set. "name": "John Doe", "token": "HkrptwS42ZBXdRKj1TyUo", "documentDeletedAt": null, - "expired": null, + "expiresAt": null, + "expirationNotifiedAt": 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" + } + ], + "Recipient": [ + { + "id": 50, + "documentId": 10, + "templateId": null, + "email": "reviewer@example.com", + "name": "Jane Smith", + "token": "vbT8hi3jKQmrFP_LN1WcS", + "documentDeletedAt": null, + "expiresAt": null, + "expirationNotifiedAt": 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, + "expiresAt": null, + "expirationNotifiedAt": null, "signedAt": "2024-04-22T11:52:05.688Z", "authOptions": { "accessAuth": null, @@ -335,53 +537,17 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont "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, + "title": "contract.pdf", "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": [ + "recipients": [ { "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", + "signedAt": "2024-04-22T11:48:07.569Z", + "rejectionReason": "I do not agree with the terms", "readStatus": "OPENED", "signingStatus": "REJECTED", "sendStatus": "SENT" @@ -395,7 +561,9 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont ### `document.cancelled` -Triggered when the document owner cancels a pending document. +Triggered when the document owner or a team member deletes a document. Draft and pending documents are hard-deleted, while completed documents are soft-deleted. + +This event is **not** triggered when a recipient hides a document from their inbox. **Event name:** `DOCUMENT_CANCELLED` @@ -411,7 +579,6 @@ Triggered when the document owner cancels a pending document. "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, @@ -428,11 +595,35 @@ Triggered when the document owner cancels a pending document. "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": "", "signingOrder": "PARALLEL", + "allowDictateNextSigner": false, "typedSignatureEnabled": true, + "uploadSignatureEnabled": true, + "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, + "recipients": [ + { + "id": 7, + "documentId": 7, + "templateId": null, + "email": "signer@example.com", + "name": "John Doe", + "token": "XkKx1HCs6Znm2UBJA2j6o", + "documentDeletedAt": null, + "expiresAt": null, + "expirationNotifiedAt": null, + "signedAt": null, + "authOptions": { "accessAuth": null, "actionAuth": null }, + "signingOrder": 1, + "rejectionReason": null, + "role": "SIGNER", + "readStatus": "NOT_OPENED", + "signingStatus": "NOT_SIGNED", + "sendStatus": "SENT" + } + ], "Recipient": [ { "id": 7, @@ -442,7 +633,8 @@ Triggered when the document owner cancels a pending document. "name": "John Doe", "token": "XkKx1HCs6Znm2UBJA2j6o", "documentDeletedAt": null, - "expired": null, + "expiresAt": null, + "expirationNotifiedAt": null, "signedAt": null, "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 1, @@ -459,147 +651,127 @@ Triggered when the document owner cancels a pending document. } ``` ---- +### `document.reminder.sent` -## Recipient Events +Triggered when a reminder email is sent to a recipient who has not yet completed their action. -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`. +**Event name:** `DOCUMENT_REMINDER_SENT` ```json { - "event": "DOCUMENT_OPENED", + "event": "DOCUMENT_REMINDER_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, + "title": "contract.pdf", "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": [ + "recipients": [ { "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", + "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ] }, - "createdAt": "2024-04-22T11:50:26.174Z", + "createdAt": "2024-04-23T09:00:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` -### `document.signed` +--- -Triggered when a recipient signs the document. +## Template Events -**Event name:** `DOCUMENT_SIGNED` +Template events track changes to reusable document templates. Template payloads use the same structure as document payloads, with `source` set to `TEMPLATE` and `templateId` populated. -The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated. +### `template.created` + +Triggered when a new template is created. + +**Event name:** `TEMPLATE_CREATED` ```json { - "event": "DOCUMENT_SIGNED", + "event": "TEMPLATE_CREATED", "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" - } - ] + "title": "My Template", + "status": "DRAFT", + "templateId": 10, + "source": "TEMPLATE", + "recipients": [] }, - "createdAt": "2024-04-22T11:52:18.577Z", + "createdAt": "2024-04-22T11:44:44.779Z", + "webhookEndpoint": "https://your-endpoint.com/webhook" +} +``` + +### `template.updated` + +Triggered when a template's settings, recipients, or fields are modified. + +**Event name:** `TEMPLATE_UPDATED` + +```json +{ + "event": "TEMPLATE_UPDATED", + "payload": { + "id": 10, + "title": "My Updated Template", + "status": "DRAFT", + "templateId": 10, + "source": "TEMPLATE", + "recipients": [] + }, + "createdAt": "2024-04-22T12:00:00.000Z", + "webhookEndpoint": "https://your-endpoint.com/webhook" +} +``` + +### `template.deleted` + +Triggered when a template is deleted. + +**Event name:** `TEMPLATE_DELETED` + +```json +{ + "event": "TEMPLATE_DELETED", + "payload": { + "id": 10, + "title": "Deleted Template", + "status": "DRAFT", + "templateId": 10, + "source": "TEMPLATE", + "recipients": [] + }, + "createdAt": "2024-04-22T13:00:00.000Z", + "webhookEndpoint": "https://your-endpoint.com/webhook" +} +``` + +### `template.used` + +Triggered when a document is created from a template. This event fires alongside `document.created`, giving you a way to specifically track template usage. + +**Event name:** `TEMPLATE_USED` + +```json +{ + "event": "TEMPLATE_USED", + "payload": { + "id": 10, + "title": "Document from Template", + "status": "DRAFT", + "templateId": 10, + "source": "TEMPLATE", + "recipients": [] + }, + "createdAt": "2024-04-22T14:00:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` @@ -608,15 +780,28 @@ The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated. ## 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 | +### Document Events + +| Event | Trigger | Key Changes | +| ---------------------------- | ------------------------------------------- | ------------------------------------------------------------ | +| `DOCUMENT_CREATED` | Document uploaded or created from template | `status: "DRAFT"` | +| `DOCUMENT_SENT` | Document sent to recipients | `status: "PENDING"`, recipients `sendStatus: "SENT"` | +| `DOCUMENT_OPENED` | Recipient opens document for the first time | Recipient `readStatus: "OPENED"` | +| `DOCUMENT_SIGNED` | Recipient signs document | Recipient `signingStatus: "SIGNED"`, `signedAt` set | +| `DOCUMENT_RECIPIENT_COMPLETED` | Recipient completes their action | 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 or team member deletes document | Document cancelled or deleted | +| `DOCUMENT_REMINDER_SENT` | Reminder email sent to recipient | No status changes | + +### Template Events + +| Event | Trigger | Key Changes | +| ------------------ | ------------------------------------ | ------------------------- | +| `TEMPLATE_CREATED` | New template created | `source: "TEMPLATE"` | +| `TEMPLATE_UPDATED` | Template settings or fields modified | `source: "TEMPLATE"` | +| `TEMPLATE_DELETED` | Template deleted | `source: "TEMPLATE"` | +| `TEMPLATE_USED` | Document created from template | `source: "TEMPLATE"` | --- @@ -652,19 +837,22 @@ app.post('/webhook', (req, res) => { switch (event) { case 'DOCUMENT_COMPLETED': - // Handle completed document console.log(`Document ${payload.id} completed`); break; + case 'DOCUMENT_RECIPIENT_COMPLETED': + const signer = payload.recipients.find((r) => r.signingStatus === 'SIGNED'); + console.log(`${signer?.name} completed their action on document ${payload.id}`); + break; case 'DOCUMENT_SIGNED': - // Handle signature - const signer = payload.Recipient.find((r) => r.signingStatus === 'SIGNED'); - console.log(`${signer?.name} signed document ${payload.id}`); + console.log(`Signature added to document ${payload.id}`); break; case 'DOCUMENT_REJECTED': - // Handle rejection - const rejecter = payload.Recipient.find((r) => r.signingStatus === 'REJECTED'); + const rejecter = payload.recipients.find((r) => r.signingStatus === 'REJECTED'); console.log(`${rejecter?.name} rejected: ${rejecter?.rejectionReason}`); break; + case 'TEMPLATE_USED': + console.log(`Template ${payload.templateId} used to create document ${payload.id}`); + break; } res.status(200).send('OK'); diff --git a/apps/docs/content/docs/developers/webhooks/index.mdx b/apps/docs/content/docs/developers/webhooks/index.mdx index ef842feca..14bb89123 100644 --- a/apps/docs/content/docs/developers/webhooks/index.mdx +++ b/apps/docs/content/docs/developers/webhooks/index.mdx @@ -1,6 +1,6 @@ --- title: Webhooks -description: Receive real-time notifications when documents are signed, completed, or updated. +description: Receive real-time notifications for document and template events. --- ## How Webhooks Work @@ -9,6 +9,8 @@ description: Receive real-time notifications when documents are signed, complete 2. When an event occurs, Documenso sends an HTTP POST to your URL 3. Your application processes the event and responds with 200 OK +Documenso supports webhook events for the full document lifecycle (created, sent, opened, signed, completed, rejected, cancelled) as well as template events (created, updated, deleted, used). + --- ## Getting Started @@ -41,7 +43,15 @@ description: Receive real-time notifications when documents are signed, complete "payload": { "id": 123, "title": "Contract", - "status": "COMPLETED" + "status": "COMPLETED", + "completedAt": "2024-01-15T10:30:00.000Z", + "recipients": [ + { + "id": 1, + "email": "signer@example.com", + "signingStatus": "SIGNED" + } + ] }, "createdAt": "2024-01-15T10:30:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" diff --git a/apps/docs/content/docs/developers/webhooks/setup.mdx b/apps/docs/content/docs/developers/webhooks/setup.mdx index db091b047..1725bec05 100644 --- a/apps/docs/content/docs/developers/webhooks/setup.mdx +++ b/apps/docs/content/docs/developers/webhooks/setup.mdx @@ -220,11 +220,17 @@ When creating a webhook, you can subscribe to one or more events: | ----- | ------- | | `DOCUMENT_CREATED` | A new document is created | | `DOCUMENT_SENT` | A document is sent to recipients | -| `DOCUMENT_OPENED` | A recipient opens the document | +| `DOCUMENT_OPENED` | A recipient opens the document for the first time | | `DOCUMENT_SIGNED` | A recipient signs the document | -| `DOCUMENT_COMPLETED` | All recipients have signed the document | +| `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action | +| `DOCUMENT_COMPLETED` | All recipients have completed their actions | | `DOCUMENT_REJECTED` | A recipient rejects the document | -| `DOCUMENT_CANCELLED` | The document owner cancels the document | +| `DOCUMENT_CANCELLED` | The document owner deletes the document | +| `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient | +| `TEMPLATE_CREATED` | A new template is created | +| `TEMPLATE_UPDATED` | A template is modified | +| `TEMPLATE_DELETED` | A template is deleted | +| `TEMPLATE_USED` | A document is created from a template | 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`. diff --git a/apps/docs/content/docs/developers/webhooks/verification.mdx b/apps/docs/content/docs/developers/webhooks/verification.mdx index 8628cbae8..a6cac916d 100644 --- a/apps/docs/content/docs/developers/webhooks/verification.mdx +++ b/apps/docs/content/docs/developers/webhooks/verification.mdx @@ -250,9 +250,15 @@ const validEvents = [ 'DOCUMENT_SENT', 'DOCUMENT_OPENED', 'DOCUMENT_SIGNED', + 'DOCUMENT_RECIPIENT_COMPLETED', 'DOCUMENT_COMPLETED', 'DOCUMENT_REJECTED', 'DOCUMENT_CANCELLED', + 'DOCUMENT_REMINDER_SENT', + 'TEMPLATE_CREATED', + 'TEMPLATE_UPDATED', + 'TEMPLATE_DELETED', + 'TEMPLATE_USED', ]; if (!validEvents.includes(event)) { diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 51826f9ae..9b56d2061 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -109,7 +109,9 @@ export const completeDocumentWithToken = async ({ } if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { - const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token }); + const isRecipientsTurn = await getIsRecipientsTurnToSign({ + token: recipient.token, + }); if (!isRecipientsTurn) { throw new Error( @@ -286,6 +288,18 @@ export const completeDocumentWithToken = async ({ }); }); + const envelopeWithRelations = await prisma.envelope.findUniqueOrThrow({ + where: { id: envelope.id }, + include: { documentMeta: true, recipients: true }, + }); + + await triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations)), + userId: envelope.userId, + teamId: envelope.teamId, + }); + await jobs.triggerJob({ name: 'send.recipient.signed.email', payload: { diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index acfe4d599..5e3a77d90 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -93,6 +93,13 @@ export const deleteDocument = async ({ user, requestMetadata, }); + + await triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_CANCELLED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), + userId, + teamId, + }); } // Continue to hide the document from the user if they are a recipient. @@ -112,13 +119,6 @@ export const deleteDocument = async ({ }); } - await triggerWebhook({ - event: WebhookTriggerEvents.DOCUMENT_CANCELLED, - data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), - userId, - teamId, - }); - return envelope; }; diff --git a/packages/lib/server-only/document/resend-document.ts b/packages/lib/server-only/document/resend-document.ts index 6f610da54..b71d21253 100644 --- a/packages/lib/server-only/document/resend-document.ts +++ b/packages/lib/server-only/document/resend-document.ts @@ -7,6 +7,7 @@ import { OrganisationType, RecipientRole, SigningStatus, + WebhookTriggerEvents, } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; @@ -25,12 +26,17 @@ import { prisma } from '@documenso/prisma'; import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; +import { + ZWebhookDocumentSchema, + mapEnvelopeToWebhookDocumentPayload, +} from '../../types/webhook-payload'; import { isDocumentCompleted } from '../../utils/document'; import type { EnvelopeIdOptions } from '../../utils/envelope'; import { isRecipientEmailValidForSending } from '../../utils/recipients'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; +import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export type ResendDocumentOptions = { id: EnvelopeIdOptions; @@ -250,5 +256,12 @@ export const resendDocument = async ({ }), ); + await triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_REMINDER_SENT, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), + userId: envelope.userId, + teamId: envelope.teamId, + }); + return envelope; }; diff --git a/packages/lib/server-only/document/viewed-document.ts b/packages/lib/server-only/document/viewed-document.ts index 6be0a22b4..5bdd1828b 100644 --- a/packages/lib/server-only/document/viewed-document.ts +++ b/packages/lib/server-only/document/viewed-document.ts @@ -1,5 +1,4 @@ -import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client'; -import { WebhookTriggerEvents } from '@prisma/client'; +import { EnvelopeType, ReadStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts index ced6c5610..3eae7e6a7 100644 --- a/packages/lib/server-only/envelope/create-envelope.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -633,6 +633,13 @@ export const createEnvelope = async ({ userId, teamId, }); + } else if (type === EnvelopeType.TEMPLATE) { + await triggerWebhook({ + event: WebhookTriggerEvents.TEMPLATE_CREATED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)), + userId, + teamId, + }); } return createdEnvelope; diff --git a/packages/lib/server-only/envelope/update-envelope.ts b/packages/lib/server-only/envelope/update-envelope.ts index d2cf256bd..c1ce03cac 100644 --- a/packages/lib/server-only/envelope/update-envelope.ts +++ b/packages/lib/server-only/envelope/update-envelope.ts @@ -1,6 +1,5 @@ import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client'; -import { EnvelopeType, FolderType } from '@prisma/client'; -import { DocumentStatus } from '@prisma/client'; +import { DocumentStatus, EnvelopeType, FolderType, WebhookTriggerEvents } from '@prisma/client'; import { isDeepEqual } from 'remeda'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; @@ -12,9 +11,14 @@ import { prisma } from '@documenso/prisma'; import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; +import { + ZWebhookDocumentSchema, + mapEnvelopeToWebhookDocumentPayload, +} from '../../types/webhook-payload'; import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth'; import type { EnvelopeIdOptions } from '../../utils/envelope'; import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams'; +import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { getEnvelopeWhereInput } from './get-envelope-by-id'; export type UpdateEnvelopeOptions = { @@ -309,8 +313,8 @@ export const updateEnvelope = async ({ // return envelope; // } - return await prisma.$transaction(async (tx) => { - const updatedEnvelope = await tx.envelope.update({ + const updatedEnvelope = await prisma.$transaction(async (tx) => { + const result = await tx.envelope.update({ where: { id: envelope.id, }, @@ -331,6 +335,10 @@ export const updateEnvelope = async ({ }, }, }, + include: { + documentMeta: true, + recipients: true, + }, }); if (envelope.type === EnvelopeType.DOCUMENT) { @@ -339,6 +347,24 @@ export const updateEnvelope = async ({ }); } - return updatedEnvelope; + return result; }); + + if (envelope.type === EnvelopeType.TEMPLATE) { + await triggerWebhook({ + event: WebhookTriggerEvents.TEMPLATE_UPDATED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)), + userId, + teamId, + }); + } + + // deconstruct to remove the recipients and documentMeta from the returned object since they aren't needed and can be large. + const { + recipients: _recipients, + documentMeta: _documentMeta, + ...finalEnvelope + } = updatedEnvelope; + + return finalEnvelope; }; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index b1d6de632..3f5ca0723 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -766,12 +766,20 @@ export const createDocumentFromTemplate = async ({ // Trigger webhook outside the transaction to avoid holding the connection // open during network I/O. - await triggerWebhook({ - event: WebhookTriggerEvents.DOCUMENT_CREATED, - data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)), - userId, - teamId, - }); + await Promise.allSettled([ + triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_CREATED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)), + userId, + teamId, + }), + triggerWebhook({ + event: WebhookTriggerEvents.TEMPLATE_USED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)), + userId, + teamId, + }), + ]); return envelope; }; diff --git a/packages/lib/server-only/template/delete-template.ts b/packages/lib/server-only/template/delete-template.ts index 7833a389b..4ea6a28a7 100644 --- a/packages/lib/server-only/template/delete-template.ts +++ b/packages/lib/server-only/template/delete-template.ts @@ -1,9 +1,14 @@ -import { EnvelopeType } from '@prisma/client'; +import { EnvelopeType, WebhookTriggerEvents } from '@prisma/client'; import { prisma } from '@documenso/prisma'; +import { + ZWebhookDocumentSchema, + mapEnvelopeToWebhookDocumentPayload, +} from '../../types/webhook-payload'; import { type EnvelopeIdOptions } from '../../utils/envelope'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; +import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export type DeleteTemplateOptions = { id: EnvelopeIdOptions; @@ -19,7 +24,21 @@ export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptio teamId, }); - return await prisma.envelope.delete({ + const templateToDelete = await prisma.envelope.findUniqueOrThrow({ + where: envelopeWhereInput, + include: { documentMeta: true, recipients: true }, + }); + + const deletedTemplate = await prisma.envelope.delete({ where: envelopeWhereInput, }); + + await triggerWebhook({ + event: WebhookTriggerEvents.TEMPLATE_DELETED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(templateToDelete)), + userId, + teamId, + }); + + return deletedTemplate; }; diff --git a/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts b/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts index 86b63a23f..ec736d17a 100644 --- a/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts +++ b/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts @@ -545,5 +545,119 @@ export const generateSampleWebhookPayload = ( }; } + if (event === WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED) { + return { + event, + payload: { + ...basePayload, + status: DocumentStatus.PENDING, + recipients: [ + { + ...basePayload.recipients[0], + readStatus: ReadStatus.OPENED, + signingStatus: SigningStatus.SIGNED, + signedAt: now, + }, + ], + Recipient: [ + { + ...basePayload.recipients[0], + readStatus: ReadStatus.OPENED, + signingStatus: SigningStatus.SIGNED, + signedAt: now, + }, + ], + }, + createdAt: now.toISOString(), + webhookEndpoint: webhookUrl, + }; + } + + if (event === WebhookTriggerEvents.DOCUMENT_REMINDER_SENT) { + return { + event, + payload: { + ...basePayload, + status: DocumentStatus.PENDING, + recipients: [ + { + ...basePayload.recipients[0], + sendStatus: SendStatus.SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + ], + Recipient: [ + { + ...basePayload.recipients[0], + sendStatus: SendStatus.SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + ], + }, + createdAt: now.toISOString(), + webhookEndpoint: webhookUrl, + }; + } + + if (event === WebhookTriggerEvents.TEMPLATE_CREATED) { + return { + event, + payload: { + ...basePayload, + title: 'My Template', + status: DocumentStatus.DRAFT, + templateId: 10, + source: DocumentSource.TEMPLATE, + }, + createdAt: now.toISOString(), + webhookEndpoint: webhookUrl, + }; + } + + if (event === WebhookTriggerEvents.TEMPLATE_UPDATED) { + return { + event, + payload: { + ...basePayload, + title: 'My Updated Template', + status: DocumentStatus.DRAFT, + templateId: 10, + source: DocumentSource.TEMPLATE, + }, + createdAt: now.toISOString(), + webhookEndpoint: webhookUrl, + }; + } + + if (event === WebhookTriggerEvents.TEMPLATE_DELETED) { + return { + event, + payload: { + ...basePayload, + title: 'Deleted Template', + status: DocumentStatus.DRAFT, + templateId: 10, + source: DocumentSource.TEMPLATE, + }, + createdAt: now.toISOString(), + webhookEndpoint: webhookUrl, + }; + } + + if (event === WebhookTriggerEvents.TEMPLATE_USED) { + return { + event, + payload: { + ...basePayload, + title: 'Document from Template', + status: DocumentStatus.DRAFT, + templateId: 10, + source: DocumentSource.TEMPLATE, + }, + createdAt: now.toISOString(), + webhookEndpoint: webhookUrl, + }; + } + throw new Error(`Unsupported event type: ${event}`); }; diff --git a/packages/prisma/migrations/20251101000330_add_new_webhook_events/migration.sql b/packages/prisma/migrations/20251101000330_add_new_webhook_events/migration.sql new file mode 100644 index 000000000..8e27a5ee8 --- /dev/null +++ b/packages/prisma/migrations/20251101000330_add_new_webhook_events/migration.sql @@ -0,0 +1,14 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_RECIPIENT_COMPLETED'; +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_REMINDER_SENT'; +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_CREATED'; +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_UPDATED'; +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_DELETED'; +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_USED'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index d41931a5c..bc1803eba 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -173,6 +173,12 @@ enum WebhookTriggerEvents { DOCUMENT_REJECTED DOCUMENT_CANCELLED RECIPIENT_EXPIRED + DOCUMENT_RECIPIENT_COMPLETED + DOCUMENT_REMINDER_SENT + TEMPLATE_CREATED + TEMPLATE_UPDATED + TEMPLATE_DELETED + TEMPLATE_USED } model Webhook {