mirror of
https://github.com/documenso/documenso.git
synced 2026-08-15 02:53:32 +10:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231dc599fe | ||
|
|
b076a70d98 | ||
|
|
6ace46fefd | ||
|
|
478229aa90 | ||
|
|
d3eb0c7999 | ||
|
|
918e42b992 | ||
|
|
f21eddef19 |
@@ -28,35 +28,62 @@ Each document contains one or more PDF files, a list of recipients, and the fiel
|
||||
|
||||
A document object contains the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | -------------- | -------------------------------------------------------------- |
|
||||
| `id` | string | Unique identifier (e.g., `envelope_abc123`) |
|
||||
| `type` | string | `DOCUMENT` or `TEMPLATE` |
|
||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, or `REJECTED` |
|
||||
| `title` | string | Document title |
|
||||
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `API` |
|
||||
| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` |
|
||||
| `externalId` | string \| null | Your custom identifier for the document |
|
||||
| `createdAt` | string | ISO 8601 timestamp |
|
||||
| `updatedAt` | string | ISO 8601 timestamp |
|
||||
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
||||
| `deletedAt` | string \| null | Timestamp if soft-deleted |
|
||||
| `recipients` | array | List of recipients and their signing status |
|
||||
| `fields` | array | Signature and form fields on the document |
|
||||
| `envelopeItems` | array | PDF files attached to the document |
|
||||
| `documentMeta` | object | Email settings, redirect URL, signing options |
|
||||
| Property | Type | Description |
|
||||
| ------------------- | -------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `id` | string | Unique identifier (e.g., `envelope_abc123`) |
|
||||
| `secondaryId` | string | Legacy identifier in prefixed form (`document_123` for documents, `template_123` for templates) |
|
||||
| `internalVersion` | number | Internal envelope schema version |
|
||||
| `type` | string | `DOCUMENT` or `TEMPLATE` |
|
||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, or `CANCELLED` |
|
||||
| `title` | string | Document title |
|
||||
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` |
|
||||
| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` |
|
||||
| `templateType` | string | Template visibility: `PUBLIC`, `PRIVATE`, or `ORGANISATION` (only meaningful for templates) |
|
||||
| `externalId` | string \| null | Your custom identifier for the document |
|
||||
| `userId` | number | ID of the user who owns the document |
|
||||
| `teamId` | number | ID of the team the document belongs to |
|
||||
| `folderId` | string \| null | ID of the folder containing the document |
|
||||
| `templateId` | number \| null | Legacy ID of the template this document was created from |
|
||||
| `authOptions` | object \| null | Access and action authentication requirements |
|
||||
| `formValues` | object \| null | Pre-filled form values |
|
||||
| `publicTitle` | string | Public title shown on profile and direct-link pages |
|
||||
| `publicDescription` | string | Public description shown on profile and direct-link pages |
|
||||
| `createdAt` | string | ISO 8601 timestamp |
|
||||
| `updatedAt` | string | ISO 8601 timestamp |
|
||||
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
||||
| `deletedAt` | string \| null | Timestamp if soft-deleted |
|
||||
| `recipients` | array | List of recipients and their signing status |
|
||||
| `fields` | array | Signature and form fields on the document |
|
||||
| `envelopeItems` | array | PDF files attached to the document |
|
||||
| `directLink` | object \| null | Direct-link signing configuration (`id`, `token`, `enabled`, `directTemplateRecipientId`) |
|
||||
| `team` | object | Owning team (`id`, `url`) |
|
||||
| `user` | object | Document owner (`id`, `name`, `email`) |
|
||||
| `documentMeta` | object | Email settings, redirect URL, signing options |
|
||||
|
||||
Documents created through the API have `source: "DOCUMENT"` — there is no separate `API` source value. To tag documents created by your integration, set `externalId` when creating them.
|
||||
|
||||
### Example Document Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "envelope_abc123xyz",
|
||||
"secondaryId": "document_123",
|
||||
"internalVersion": 2,
|
||||
"type": "DOCUMENT",
|
||||
"status": "PENDING",
|
||||
"source": "API",
|
||||
"source": "DOCUMENT",
|
||||
"visibility": "EVERYONE",
|
||||
"templateType": "PRIVATE",
|
||||
"title": "Service Agreement",
|
||||
"externalId": "contract-2025-001",
|
||||
"userId": 1,
|
||||
"teamId": 1,
|
||||
"folderId": null,
|
||||
"templateId": null,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"publicTitle": "",
|
||||
"publicDescription": "",
|
||||
"createdAt": "2025-01-15T10:30:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:35:00.000Z",
|
||||
"completedAt": null,
|
||||
@@ -73,23 +100,41 @@ A document object contains the following properties:
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"id": "field_123",
|
||||
"id": 123,
|
||||
"secondaryId": "field_abc123",
|
||||
"type": "SIGNATURE",
|
||||
"recipientId": 1,
|
||||
"envelopeId": "envelope_abc123xyz",
|
||||
"envelopeItemId": "envelope_item_xyz",
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5,
|
||||
"recipientId": 1
|
||||
"positionX": "10",
|
||||
"positionY": "80",
|
||||
"width": "30",
|
||||
"height": "5",
|
||||
"customText": "",
|
||||
"inserted": false,
|
||||
"fieldMeta": null
|
||||
}
|
||||
],
|
||||
"envelopeItems": [
|
||||
{
|
||||
"id": "envelope_item_xyz",
|
||||
"envelopeId": "envelope_abc123xyz",
|
||||
"documentDataId": "doc_data_abc123",
|
||||
"title": "contract.pdf",
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"directLink": null,
|
||||
"team": {
|
||||
"id": 1,
|
||||
"url": "your-team"
|
||||
},
|
||||
"user": {
|
||||
"id": 1,
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com"
|
||||
},
|
||||
"documentMeta": {
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hi, please review and sign this agreement.",
|
||||
@@ -99,6 +144,8 @@ A document object contains the following properties:
|
||||
}
|
||||
```
|
||||
|
||||
Field position and size values are stored as decimals and serialized as strings in API responses.
|
||||
|
||||
## List Documents
|
||||
|
||||
Retrieve a paginated list of documents.
|
||||
@@ -114,7 +161,7 @@ GET /envelope
|
||||
| `page` | integer | Page number (default: 1) |
|
||||
| `perPage` | integer | Results per page (default: 10, max: 100) |
|
||||
| `type` | string | Filter by `DOCUMENT` or `TEMPLATE` |
|
||||
| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` |
|
||||
| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` |
|
||||
| `source` | string | Filter by creation source |
|
||||
| `folderId` | string | Filter by folder ID |
|
||||
| `orderByColumn` | string | Sort field (only `createdAt` supported) |
|
||||
@@ -154,8 +201,8 @@ const response = await fetch(`${BASE_URL}/envelope`, {
|
||||
},
|
||||
});
|
||||
|
||||
const { data, pagination } = await response.json();
|
||||
console.log(`Found ${pagination.totalItems} documents`);
|
||||
const { data, count } = await response.json();
|
||||
console.log(`Found ${count} documents`);
|
||||
|
||||
// Filter by status
|
||||
const pendingResponse = await fetch(
|
||||
@@ -197,12 +244,10 @@ const pendingDocs = await pendingResponse.json();
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"perPage": 10,
|
||||
"totalPages": 5,
|
||||
"totalItems": 42
|
||||
}
|
||||
"count": 42,
|
||||
"currentPage": 1,
|
||||
"perPage": 10,
|
||||
"totalPages": 5
|
||||
}
|
||||
```
|
||||
|
||||
@@ -628,6 +673,72 @@ The response includes signing URLs for each recipient:
|
||||
|
||||
---
|
||||
|
||||
## Cancel Document
|
||||
|
||||
Cancel a pending document. This changes its status from `PENDING` to `CANCELLED`.
|
||||
|
||||
```
|
||||
POST /envelope/cancel
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | ----------------------------------- |
|
||||
| `envelopeId` | string | Yes | Document ID |
|
||||
| `reason` | string | No | Reason for cancelling the document |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/cancel" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "envelope_abc123",
|
||||
"reason": "The agreement is no longer needed."
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/cancel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'envelope_abc123',
|
||||
reason: 'The agreement is no longer needed.',
|
||||
}),
|
||||
});
|
||||
|
||||
const { success } = await response.json();
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- Only documents in `PENDING` status can be cancelled. Other statuses return `400`.
|
||||
- Cancellation is not idempotent. Cancelling the same document again returns `400`.
|
||||
- The document owner and team members with `MANAGER` or higher permissions can cancel it. Requests for documents you cannot view return `404`; requests for visible documents without sufficient permissions return `401`.
|
||||
- A successful cancellation fires the `DOCUMENT_CANCELLED` webhook.
|
||||
- Cancellation emails are sent only to eligible non-CC, non-rejected recipients who were sent or opened the document.
|
||||
|
||||
---
|
||||
|
||||
## Delete Document
|
||||
|
||||
Delete a document. Completed documents cannot be deleted.
|
||||
@@ -670,7 +781,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/delete',
|
||||
|
||||
const { success } = await response.json();
|
||||
|
||||
````
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -680,7 +791,7 @@ const { success } = await response.json();
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
````
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -694,9 +805,11 @@ POST /envelope/get-many
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------- | ----- | -------- | --------------------- |
|
||||
| `envelopeIds` | array | Yes | Array of document IDs |
|
||||
| Field | Type | Required | Description |
|
||||
| ---------- | ------ | -------- | ---------------------------------------------------------------------------- |
|
||||
| `ids` | object | Yes | ID selector containing `type` and `ids` |
|
||||
| `ids.type` | string | Yes | `envelopeId`, `documentId`, or `templateId` |
|
||||
| `ids.ids` | array | Yes | 1-20 IDs: strings for `envelopeId`; numbers for `documentId` or `templateId` |
|
||||
|
||||
### Code Examples
|
||||
|
||||
@@ -707,12 +820,17 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/get-many" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeIds": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
||||
"ids": {
|
||||
"type": "envelopeId",
|
||||
"ids": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const requestedIds = ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'];
|
||||
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -720,16 +838,36 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeIds: ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'],
|
||||
ids: {
|
||||
type: 'envelopeId',
|
||||
ids: requestedIds,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const documents = await response.json();
|
||||
const { data } = await response.json();
|
||||
|
||||
````
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "envelope_abc123",
|
||||
"type": "DOCUMENT",
|
||||
"status": "PENDING",
|
||||
"title": "Service Agreement"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint silently omits envelopes you cannot access instead of returning `404`. Compare `data.length` with `requestedIds.length` to detect omissions.
|
||||
|
||||
---
|
||||
|
||||
## Document Statuses
|
||||
@@ -740,6 +878,7 @@ const documents = await response.json();
|
||||
| `PENDING` | Document has been sent. Waiting for recipients to sign. |
|
||||
| `COMPLETED` | All recipients have signed. Document is sealed. |
|
||||
| `REJECTED` | A recipient rejected the document. |
|
||||
| `CANCELLED` | The document was cancelled by its owner or a team member with `MANAGER` or higher permissions. |
|
||||
|
||||
### Status Transitions
|
||||
|
||||
@@ -747,11 +886,13 @@ const documents = await response.json();
|
||||
flowchart LR
|
||||
DRAFT --> PENDING --> COMPLETED
|
||||
PENDING --> REJECTED
|
||||
PENDING --> CANCELLED
|
||||
```
|
||||
|
||||
- **DRAFT to PENDING**: Call the distribute endpoint
|
||||
- **PENDING to COMPLETED**: All recipients complete their signing
|
||||
- **PENDING to REJECTED**: A recipient rejects the document
|
||||
- **PENDING to CANCELLED**: The document owner or a team member with `MANAGER` or higher permissions cancels the document
|
||||
|
||||
<Callout type="warn">
|
||||
You cannot modify recipients or fields after a document moves to `PENDING` status.
|
||||
@@ -773,8 +914,8 @@ flowchart LR
|
||||
| Parameter | Values | Description |
|
||||
| ---------- | ------------------------------------------- | ------------------------- |
|
||||
| `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type |
|
||||
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` | Filter by status |
|
||||
| `source` | `DOCUMENT`, `TEMPLATE`, `API` | Filter by creation source |
|
||||
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | Filter by status |
|
||||
| `source` | `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | Filter by creation source |
|
||||
| `folderId` | string | Filter by folder |
|
||||
|
||||
### Sorting
|
||||
@@ -800,10 +941,10 @@ async function getAllPendingDocuments() {
|
||||
},
|
||||
);
|
||||
|
||||
const { data, pagination } = await response.json();
|
||||
const { data, currentPage, totalPages } = await response.json();
|
||||
documents.push(...data);
|
||||
|
||||
hasMore = page < pagination.totalPages;
|
||||
hasMore = currentPage < totalPages;
|
||||
page++;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com).
|
||||
| ------------------------------------------------- | ----------------------------------------------------- |
|
||||
| `GET /api/v2/document` | `GET /api/v2/envelope` |
|
||||
| `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||
| `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` |
|
||||
| `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` (body changes from `documentIds: number[]` to `ids: { type: "documentId"; ids: number[] }`) |
|
||||
| `POST /api/v2/document/create` | `POST /api/v2/envelope/create` |
|
||||
| `POST /api/v2/document/create/beta` | `POST /api/v2/envelope/create` |
|
||||
| `POST /api/v2/document/update` | `POST /api/v2/envelope/update` |
|
||||
@@ -140,7 +140,7 @@ Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com).
|
||||
| ------------------------------------- | ------------------------------------------------ |
|
||||
| `GET /api/v2/template` | `GET /api/v2/envelope` (with `type=TEMPLATE`) |
|
||||
| `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||
| `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` |
|
||||
| `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` (body changes from `templateIds: number[]` to `ids: { type: "templateId"; ids: number[] }`) |
|
||||
| `POST /api/v2/template/create` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||
| `POST /api/v2/template/create/beta` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||
| `POST /api/v2/template/update` | `POST /api/v2/envelope/update` |
|
||||
|
||||
@@ -11,6 +11,12 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
||||
|
||||
## HTTP Rate Limits
|
||||
|
||||
The rate limit applies to:
|
||||
|
||||
- `/api/v1/*`
|
||||
- `/api/v2/*`
|
||||
- `/api/v2-beta/*`
|
||||
|
||||
**Limit:** 1000 requests per minute per IP address
|
||||
**Response:** 429 Too Many Requests
|
||||
|
||||
@@ -19,7 +25,7 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
||||
this value, in which case you can be rate-limited before reaching the global limit.
|
||||
</Callout>
|
||||
|
||||
### Rate Limit Response
|
||||
### Global per-IP 429 Response
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -27,10 +33,22 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
No rate limit headers are currently provided. When you receive a 429 response, wait at least 60
|
||||
seconds before retrying.
|
||||
</Callout>
|
||||
### Rate Limit Headers
|
||||
|
||||
Responses from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` include these headers. The only
|
||||
exception is CORS preflight (`OPTIONS`) requests, which are answered before the rate limiter runs
|
||||
and carry no rate limit headers:
|
||||
|
||||
| Header | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------- |
|
||||
| `X-RateLimit-Limit` | Maximum requests allowed in the current global window |
|
||||
| `X-RateLimit-Remaining` | Requests remaining in the current global window |
|
||||
| `X-RateLimit-Reset` | End of the current global window, as a Unix epoch timestamp in seconds |
|
||||
|
||||
A 429 response from a windowed limiter also includes `Retry-After`, in seconds, with a minimum
|
||||
value of `1`. The global API limit uses fixed, epoch-aligned one-minute buckets, so the actual wait
|
||||
until the next window is between 1 and 60 seconds. Honor `Retry-After` exactly instead of sleeping
|
||||
for a fixed 60 seconds. See the [Retry-After handling example](/docs/developers/examples/common-workflows#error-handling-patterns).
|
||||
|
||||
## Resource Limits
|
||||
|
||||
@@ -44,24 +62,55 @@ Beyond HTTP rate limits, your account has usage limits based on your subscriptio
|
||||
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
|
||||
| Direct Templates | 3 | Unlimited | Unlimited | Unlimited |
|
||||
|
||||
### Error Response
|
||||
### Organisation Limit 429 Responses
|
||||
|
||||
When you exceed a resource limit:
|
||||
Organisation windowed limits and organisation monthly quotas produce 429 responses whose body
|
||||
shape depends on the API version, and neither matches the global per-IP limiter's
|
||||
`{ "error": "..." }` body.
|
||||
|
||||
On `/api/v1/*`, the body contains only a message:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "You have reached your document limit for this month. Please upgrade your plan.",
|
||||
"code": "LIMIT_EXCEEDED",
|
||||
"statusCode": 400
|
||||
"message": "Too many requests, please try again later. Contact support if you require higher limits."
|
||||
}
|
||||
```
|
||||
|
||||
On `/api/v2/*` and `/api/v2-beta/*`, the body is a structured error object:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Too many requests, please try again later. Contact support if you require higher limits.",
|
||||
"code": "TOO_MANY_REQUESTS",
|
||||
"data": {
|
||||
"code": "TOO_MANY_REQUESTS",
|
||||
"httpStatus": 429,
|
||||
"appError": {
|
||||
"code": "TOO_MANY_REQUESTS",
|
||||
"message": "Too many requests, please try again later. Contact support if you require higher limits."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Organisation windowed limit responses include the `X-RateLimit-*` headers and `Retry-After` for
|
||||
their own window. Monthly quota responses carry no quota-specific rate limit headers or
|
||||
`Retry-After` because the quota is not a time window; rely on the status code and message instead.
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Status | Description |
|
||||
| ------------------- | ------ | ----------------------------- |
|
||||
| `TOO_MANY_REQUESTS` | 429 | HTTP rate limit exceeded |
|
||||
| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded |
|
||||
| Code | Status | Description |
|
||||
| ------------------- | ------ | ------------------------------------------------------------------ |
|
||||
| `TOO_MANY_REQUESTS` | 429 | Global per-IP, organisation windowed, or monthly quota exceeded |
|
||||
| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded |
|
||||
|
||||
There are three sources of `TOO_MANY_REQUESTS` responses:
|
||||
|
||||
1. The global per-IP limit, returning the `{ "error": "..." }` body shown above.
|
||||
2. Organisation windowed rate limits for the `api`, `document`, and `email` counters.
|
||||
3. Organisation monthly quotas for the same three counters. Every authenticated API request
|
||||
consumes the `api` counter, so any endpoint can return this 429 once the monthly API quota is
|
||||
exhausted — not just envelope-related ones.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1000,9 +1000,12 @@ async function fetchWithRetry(
|
||||
// Retry on rate limit
|
||||
if (response.status === 429) {
|
||||
const retryAfter = response.headers.get('Retry-After');
|
||||
const delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelayMs * Math.pow(2, attempt);
|
||||
// Honor Retry-After exactly; the cap only applies to the exponential fallback.
|
||||
const delay = retryAfter
|
||||
? parseInt(retryAfter) * 1000
|
||||
: Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
||||
console.log(`Rate limited, waiting ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs)));
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,13 +33,14 @@ All webhook events share a common structure:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------- | --------- | ------------------------------------------------------ |
|
||||
| `id` | number | Document or template ID |
|
||||
| `id` | number | Legacy numeric v1 document or template ID |
|
||||
| `envelopeId` | string | Canonical v2 identifier (`envelope_` + 16 characters) |
|
||||
| `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 or template title |
|
||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED` |
|
||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` |
|
||||
| `visibility` | string | Document visibility setting |
|
||||
| `createdAt` | datetime | Document creation timestamp |
|
||||
| `updatedAt` | datetime | Last modification timestamp |
|
||||
@@ -47,8 +48,8 @@ All webhook events share a common structure:
|
||||
| `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) |
|
||||
| `source` | string | Source: `DOCUMENT`, `TEMPLATE`, or `TEMPLATE_DIRECT_LINK` |
|
||||
| `documentMeta` | object? | Nullable document metadata (subject, message, signing options) |
|
||||
| `recipients` | array | List of recipient objects |
|
||||
| `Recipient` | array | List of recipient objects (legacy, same as recipients) |
|
||||
|
||||
@@ -60,7 +61,6 @@ All webhook events share a common structure:
|
||||
| `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` |
|
||||
@@ -77,8 +77,9 @@ All webhook events share a common structure:
|
||||
| Field | Type | Description |
|
||||
| ---------------------- | --------- | ------------------------------------------ |
|
||||
| `id` | number | Recipient ID |
|
||||
| `documentId` | number? | Parent document ID |
|
||||
| `templateId` | number? | Template ID if created from a template |
|
||||
| `envelopeId` | string | Canonical parent envelope ID |
|
||||
| `documentId` | number? | Legacy parent document ID; null for templates |
|
||||
| `templateId` | number? | Legacy parent template ID; null for documents |
|
||||
| `email` | string | Recipient email address |
|
||||
| `name` | string | Recipient name |
|
||||
| `token` | string | Unique signing token |
|
||||
@@ -94,6 +95,8 @@ All webhook events share a common structure:
|
||||
| `sendStatus` | string | `NOT_SENT` or `SENT` |
|
||||
| `rejectionReason` | string? | Reason if recipient rejected |
|
||||
|
||||
Use `recipient.envelopeId` as the reliable parent link. The legacy `documentId` and `templateId` fields depend on the parent envelope type, so one of them is always null.
|
||||
|
||||
---
|
||||
|
||||
## Document Lifecycle Events
|
||||
@@ -111,6 +114,7 @@ Triggered when a new document is created.
|
||||
"event": "DOCUMENT_CREATED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
@@ -129,9 +133,8 @@ Triggered when a new document is created.
|
||||
"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",
|
||||
"timezone": "Etc/UTC",
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
@@ -145,6 +148,7 @@ Triggered when a new document is created.
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -166,6 +170,7 @@ Triggered when a new document is created.
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -203,6 +208,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"event": "DOCUMENT_SENT",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
@@ -221,9 +227,8 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"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",
|
||||
"timezone": "Etc/UTC",
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
@@ -237,6 +242,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -258,6 +264,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -295,12 +302,14 @@ The recipient's `readStatus` changes to `OPENED`.
|
||||
"event": "DOCUMENT_OPENED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
@@ -328,6 +337,7 @@ The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
|
||||
"event": "DOCUMENT_SIGNED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"status": "COMPLETED",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
@@ -335,6 +345,7 @@ The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
|
||||
"recipients": [
|
||||
{
|
||||
"id": 51,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
@@ -361,12 +372,14 @@ Triggered when an individual recipient completes their required action (signing,
|
||||
"event": "DOCUMENT_RECIPIENT_COMPLETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
@@ -395,6 +408,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"event": "DOCUMENT_COMPLETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
@@ -413,9 +427,8 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"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",
|
||||
"timezone": "Etc/UTC",
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
@@ -429,6 +442,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"recipients": [
|
||||
{
|
||||
"id": 50,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "reviewer@example.com",
|
||||
@@ -451,6 +465,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -475,6 +490,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 50,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "reviewer@example.com",
|
||||
@@ -497,6 +513,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -537,12 +554,14 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont
|
||||
"event": "DOCUMENT_REJECTED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
@@ -561,7 +580,7 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont
|
||||
|
||||
### `document.cancelled`
|
||||
|
||||
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.
|
||||
Triggered when a pending document is explicitly cancelled with `POST /envelope/cancel`, or when a document owner or team member deletes a document. Deleting a draft or pending document hard-deletes it, while deleting a completed document soft-deletes it.
|
||||
|
||||
This event is **not** triggered when a recipient hides a document from their inbox.
|
||||
|
||||
@@ -572,6 +591,7 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"event": "DOCUMENT_CANCELLED",
|
||||
"payload": {
|
||||
"id": 7,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"externalId": null,
|
||||
"userId": 3,
|
||||
"authOptions": null,
|
||||
@@ -591,7 +611,6 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"subject": "",
|
||||
"message": "",
|
||||
"timezone": "Etc/UTC",
|
||||
"password": null,
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": "",
|
||||
"signingOrder": "PARALLEL",
|
||||
@@ -606,6 +625,7 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"recipients": [
|
||||
{
|
||||
"id": 7,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 7,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -627,6 +647,7 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 7,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 7,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
@@ -651,6 +672,45 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
}
|
||||
```
|
||||
|
||||
### `recipient.expired`
|
||||
|
||||
Triggered when a recipient's signing deadline passes on a pending document before they sign or reject it.
|
||||
|
||||
**Event name:** `RECIPIENT_EXPIRED`
|
||||
|
||||
The recipient's `expiresAt` contains the signing deadline, and `expirationNotifiedAt` is set when the expiration is processed.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "RECIPIENT_EXPIRED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"expiresAt": "2024-04-22T11:51:00.000Z",
|
||||
"expirationNotifiedAt": "2024-04-22T11:52:00.000Z",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:52:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `document.reminder.sent`
|
||||
|
||||
Triggered when a reminder email is sent to a recipient who has not yet completed their action.
|
||||
@@ -662,12 +722,14 @@ Triggered when a reminder email is sent to a recipient who has not yet completed
|
||||
"event": "DOCUMENT_REMINDER_SENT",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
@@ -686,7 +748,7 @@ Triggered when a reminder email is sent to a recipient who has not yet completed
|
||||
|
||||
## Template Events
|
||||
|
||||
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.
|
||||
Template events track changes to reusable document templates. Template payloads use the same structure as document payloads. For `TEMPLATE_CREATED`, `TEMPLATE_UPDATED`, and `TEMPLATE_DELETED` the template's own legacy numeric ID is in `id` and `templateId` is `null`. Only `TEMPLATE_USED` — whose payload describes the new document envelope created from the template — carries the originating template's legacy ID in `templateId`, with `source` set to `TEMPLATE`.
|
||||
|
||||
### `template.created`
|
||||
|
||||
@@ -699,9 +761,10 @@ Triggered when a new template is created.
|
||||
"event": "TEMPLATE_CREATED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"title": "My Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"templateId": null,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
@@ -721,9 +784,10 @@ Triggered when a template's settings, recipients, or fields are modified.
|
||||
"event": "TEMPLATE_UPDATED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"title": "My Updated Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"templateId": null,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
@@ -743,9 +807,10 @@ Triggered when a template is deleted.
|
||||
"event": "TEMPLATE_DELETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"title": "Deleted Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"templateId": null,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
@@ -765,6 +830,7 @@ Triggered when a document is created from a template. This event fires alongside
|
||||
"event": "TEMPLATE_USED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"title": "Document from Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
@@ -791,7 +857,8 @@ Triggered when a document is created from a template. This event fires alongside
|
||||
| `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_CANCELLED` | Pending document explicitly cancelled, or document deleted | `status: "CANCELLED"` after explicit cancellation; deletion may remove or soft-delete the document |
|
||||
| `RECIPIENT_EXPIRED` | Recipient signing deadline passes | Recipient `expiresAt` passed, `expirationNotifiedAt` set |
|
||||
| `DOCUMENT_REMINDER_SENT` | Reminder email sent to recipient | No status changes |
|
||||
|
||||
### Template Events
|
||||
@@ -821,7 +888,7 @@ When processing webhook events:
|
||||
**Process idempotently** — Webhooks may be retried, so handle duplicate events
|
||||
</Step>
|
||||
<Step>
|
||||
**Respond quickly** — Return a 200 status code within 30 seconds
|
||||
**Respond quickly** — Return a `2xx` status code within 10 seconds
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
@@ -42,12 +42,14 @@ Documenso supports webhook events for the full document lifecycle (created, sent
|
||||
"event": "DOCUMENT_COMPLETED",
|
||||
"payload": {
|
||||
"id": 123,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"title": "Contract",
|
||||
"status": "COMPLETED",
|
||||
"completedAt": "2024-01-15T10:30:00.000Z",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 1,
|
||||
"envelopeId": "envelope_abcdefhiklmnorst",
|
||||
"email": "signer@example.com",
|
||||
"signingStatus": "SIGNED"
|
||||
}
|
||||
@@ -58,6 +60,8 @@ Documenso supports webhook events for the full document lifecycle (created, sent
|
||||
}
|
||||
```
|
||||
|
||||
`payload.id` is the legacy numeric v1 ID. Use `payload.envelopeId` as the canonical v2 identifier. Each recipient repeats `envelopeId` as the reliable parent link because the legacy `documentId` and `templateId` fields depend on the parent envelope type, leaving one of them null.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
@@ -148,7 +148,7 @@ func main() {
|
||||
</Tabs>
|
||||
|
||||
<Callout type="warn">
|
||||
Always respond with a `200 OK` status within 30 seconds. Documenso will retry failed deliveries.
|
||||
Always respond with a `2xx` status within 10 seconds. Documenso will retry failed deliveries according to the configured background-job provider.
|
||||
</Callout>
|
||||
|
||||
## Configuring Webhooks in Documenso via the Dashboard
|
||||
@@ -184,7 +184,7 @@ Fill in the following fields:
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| **Webhook URL** | The HTTPS endpoint that will receive webhook events |
|
||||
| **Webhook URL** | The HTTP or 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>
|
||||
@@ -202,12 +202,21 @@ 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 |
|
||||
| **Protocol** | HTTP and HTTPS are accepted; use HTTPS in production |
|
||||
| **Response** | Must return a `2xx` status code within 10 seconds |
|
||||
| **Method** | Must accept HTTP POST requests |
|
||||
| **Content-Type** | Must accept `application/json` payloads |
|
||||
| **Availability** | Must be publicly accessible from the internet |
|
||||
|
||||
<Callout type="warn">
|
||||
Documenso performs a best-effort check that rejects webhook URLs which use or resolve to private
|
||||
or loopback addresses. This is not a complete SSRF mitigation — it does not cover DNS rebinding
|
||||
and fails open on DNS lookup errors or timeouts — so self-hosted deployments should still enforce
|
||||
network-level egress rules. Self-hosters that need to deliver to a hostname resolving to a
|
||||
private address can add that hostname to the comma-separated
|
||||
`NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` environment variable.
|
||||
</Callout>
|
||||
|
||||
<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>
|
||||
@@ -225,7 +234,8 @@ When creating a webhook, you can subscribe to one or more events:
|
||||
| `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 deletes the document |
|
||||
| `DOCUMENT_CANCELLED` | A pending document is explicitly cancelled or a document owner deletes it |
|
||||
| `RECIPIENT_EXPIRED` | A recipient's signing deadline passes before they sign or reject |
|
||||
| `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient |
|
||||
| `TEMPLATE_CREATED` | A new template is created |
|
||||
| `TEMPLATE_UPDATED` | A template is modified |
|
||||
@@ -318,17 +328,17 @@ Documenso will attempt to deliver the same payload again
|
||||
|
||||
## Retry Policy
|
||||
|
||||
When a webhook delivery fails (non-2xx response or timeout), Documenso automatically retries with exponential backoff:
|
||||
A delivery fails when the endpoint returns a non-`2xx` response, the 10-second timeout expires, or the request fails. Redirects are not followed, so `3xx` responses also fail. Network and SSRF-blocked requests are recorded with response code `0`.
|
||||
|
||||
| Attempt | Delay |
|
||||
| ------- | ----- |
|
||||
| 1 | Immediate |
|
||||
| 2 | 1 minute |
|
||||
| 3 | 5 minutes |
|
||||
| 4 | 30 minutes |
|
||||
| 5 | 2 hours |
|
||||
For self-hosted deployments, retries are handled by the background-job provider selected with `NEXT_PRIVATE_JOBS_PROVIDER`:
|
||||
|
||||
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.
|
||||
| Provider | Total attempts | Retry timing |
|
||||
| -------- | -------------- | ------------ |
|
||||
| Local (default) | 4 | Back-to-back, with no backoff |
|
||||
| BullMQ | 3 | Exponential backoff starting at 1 second |
|
||||
| Inngest | 5 | Inngest platform backoff |
|
||||
|
||||
Only the individual delivery (`WebhookCall`) record is marked as failed. Documenso does not automatically disable the webhook or apply a circuit breaker, so future matching events continue to be delivered. After automatic attempts are exhausted, you can manually resend a failed delivery 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).
|
||||
|
||||
@@ -255,6 +255,7 @@ const validEvents = [
|
||||
'DOCUMENT_REJECTED',
|
||||
'DOCUMENT_CANCELLED',
|
||||
'DOCUMENT_REMINDER_SENT',
|
||||
'RECIPIENT_EXPIRED',
|
||||
'TEMPLATE_CREATED',
|
||||
'TEMPLATE_UPDATED',
|
||||
'TEMPLATE_DELETED',
|
||||
|
||||
@@ -8,13 +8,14 @@ export const cancelEnvelopeMeta: TrpcRouteMeta = {
|
||||
method: 'POST',
|
||||
path: '/envelope/cancel',
|
||||
summary: 'Cancel envelope',
|
||||
description: 'Cancel a pending envelope',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZCancelEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
reason: z.string().optional(),
|
||||
envelopeId: z.string().describe('The ID of the envelope to cancel.'),
|
||||
reason: z.string().describe('The reason for cancelling the envelope.').optional(),
|
||||
});
|
||||
|
||||
export const ZCancelEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||
|
||||
@@ -8,12 +8,13 @@ export const deleteEnvelopeMeta: TrpcRouteMeta = {
|
||||
method: 'POST',
|
||||
path: '/envelope/delete',
|
||||
summary: 'Delete envelope',
|
||||
description: 'Delete an envelope',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZDeleteEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeId: z.string().describe('The ID of the envelope to delete.'),
|
||||
});
|
||||
|
||||
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||
|
||||
@@ -12,24 +12,32 @@ export const updateEnvelopeMeta: TrpcRouteMeta = {
|
||||
method: 'POST',
|
||||
path: '/envelope/update',
|
||||
summary: 'Update envelope',
|
||||
description: 'Update envelope properties and settings',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZUpdateEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeId: z.string().describe('The ID of the envelope to update.'),
|
||||
data: z
|
||||
.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
folderId: z.string().nullish(),
|
||||
templateType: z.nativeEnum(TemplateType).optional(),
|
||||
globalAccessAuth: z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
.describe('The authentication methods required to access the envelope.')
|
||||
.optional(),
|
||||
globalActionAuth: z
|
||||
.array(ZDocumentActionAuthTypesSchema)
|
||||
.describe('The authentication methods required to sign the envelope.')
|
||||
.optional(),
|
||||
folderId: z.string().describe('The ID of the folder containing the envelope.').nullish(),
|
||||
templateType: z.nativeEnum(TemplateType).describe('The template type.').optional(),
|
||||
})
|
||||
.describe('The envelope properties to update.')
|
||||
.optional(),
|
||||
meta: ZDocumentMetaUpdateSchema.optional(),
|
||||
meta: ZDocumentMetaUpdateSchema.describe('The email and signing settings to update.').optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
|
||||
|
||||
Reference in New Issue
Block a user