mirror of
https://github.com/documenso/documenso.git
synced 2026-08-24 07:12:23 +10:00
Merge branch 'main' into feat/api-tokens-last-used-timestamp
This commit is contained in:
@@ -29,14 +29,25 @@ Each document contains one or more PDF files, a list of recipients, and the fiel
|
|||||||
A document object contains the following properties:
|
A document object contains the following properties:
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
| --------------- | -------------- | -------------------------------------------------------------- |
|
| ------------------- | -------------- | -------------------------------------------------------------------------------------------- |
|
||||||
| `id` | string | Unique identifier (e.g., `envelope_abc123`) |
|
| `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` |
|
| `type` | string | `DOCUMENT` or `TEMPLATE` |
|
||||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, or `REJECTED` |
|
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, or `CANCELLED` |
|
||||||
| `title` | string | Document title |
|
| `title` | string | Document title |
|
||||||
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `API` |
|
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` |
|
||||||
| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` |
|
| `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 |
|
| `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 |
|
| `createdAt` | string | ISO 8601 timestamp |
|
||||||
| `updatedAt` | string | ISO 8601 timestamp |
|
| `updatedAt` | string | ISO 8601 timestamp |
|
||||||
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
||||||
@@ -44,19 +55,35 @@ A document object contains the following properties:
|
|||||||
| `recipients` | array | List of recipients and their signing status |
|
| `recipients` | array | List of recipients and their signing status |
|
||||||
| `fields` | array | Signature and form fields on the document |
|
| `fields` | array | Signature and form fields on the document |
|
||||||
| `envelopeItems` | array | PDF files attached to 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 |
|
| `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
|
### Example Document Object
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "envelope_abc123xyz",
|
"id": "envelope_abc123xyz",
|
||||||
|
"secondaryId": "document_123",
|
||||||
|
"internalVersion": 2,
|
||||||
"type": "DOCUMENT",
|
"type": "DOCUMENT",
|
||||||
"status": "PENDING",
|
"status": "PENDING",
|
||||||
"source": "API",
|
"source": "DOCUMENT",
|
||||||
"visibility": "EVERYONE",
|
"visibility": "EVERYONE",
|
||||||
|
"templateType": "PRIVATE",
|
||||||
"title": "Service Agreement",
|
"title": "Service Agreement",
|
||||||
"externalId": "contract-2025-001",
|
"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",
|
"createdAt": "2025-01-15T10:30:00.000Z",
|
||||||
"updatedAt": "2025-01-15T10:35:00.000Z",
|
"updatedAt": "2025-01-15T10:35:00.000Z",
|
||||||
"completedAt": null,
|
"completedAt": null,
|
||||||
@@ -73,23 +100,41 @@ A document object contains the following properties:
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"id": "field_123",
|
"id": 123,
|
||||||
|
"secondaryId": "field_abc123",
|
||||||
"type": "SIGNATURE",
|
"type": "SIGNATURE",
|
||||||
|
"recipientId": 1,
|
||||||
|
"envelopeId": "envelope_abc123xyz",
|
||||||
|
"envelopeItemId": "envelope_item_xyz",
|
||||||
"page": 1,
|
"page": 1,
|
||||||
"positionX": 10,
|
"positionX": "10",
|
||||||
"positionY": 80,
|
"positionY": "80",
|
||||||
"width": 30,
|
"width": "30",
|
||||||
"height": 5,
|
"height": "5",
|
||||||
"recipientId": 1
|
"customText": "",
|
||||||
|
"inserted": false,
|
||||||
|
"fieldMeta": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"envelopeItems": [
|
"envelopeItems": [
|
||||||
{
|
{
|
||||||
"id": "envelope_item_xyz",
|
"id": "envelope_item_xyz",
|
||||||
|
"envelopeId": "envelope_abc123xyz",
|
||||||
|
"documentDataId": "doc_data_abc123",
|
||||||
"title": "contract.pdf",
|
"title": "contract.pdf",
|
||||||
"order": 1
|
"order": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"directLink": null,
|
||||||
|
"team": {
|
||||||
|
"id": 1,
|
||||||
|
"url": "your-team"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"email": "jane@example.com"
|
||||||
|
},
|
||||||
"documentMeta": {
|
"documentMeta": {
|
||||||
"subject": "Please sign this document",
|
"subject": "Please sign this document",
|
||||||
"message": "Hi, please review and sign this agreement.",
|
"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
|
## List Documents
|
||||||
|
|
||||||
Retrieve a paginated list of documents.
|
Retrieve a paginated list of documents.
|
||||||
@@ -114,7 +161,7 @@ GET /envelope
|
|||||||
| `page` | integer | Page number (default: 1) |
|
| `page` | integer | Page number (default: 1) |
|
||||||
| `perPage` | integer | Results per page (default: 10, max: 100) |
|
| `perPage` | integer | Results per page (default: 10, max: 100) |
|
||||||
| `type` | string | Filter by `DOCUMENT` or `TEMPLATE` |
|
| `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 |
|
| `source` | string | Filter by creation source |
|
||||||
| `folderId` | string | Filter by folder ID |
|
| `folderId` | string | Filter by folder ID |
|
||||||
| `orderByColumn` | string | Sort field (only `createdAt` supported) |
|
| `orderByColumn` | string | Sort field (only `createdAt` supported) |
|
||||||
@@ -154,8 +201,8 @@ const response = await fetch(`${BASE_URL}/envelope`, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, pagination } = await response.json();
|
const { data, count } = await response.json();
|
||||||
console.log(`Found ${pagination.totalItems} documents`);
|
console.log(`Found ${count} documents`);
|
||||||
|
|
||||||
// Filter by status
|
// Filter by status
|
||||||
const pendingResponse = await fetch(
|
const pendingResponse = await fetch(
|
||||||
@@ -197,12 +244,10 @@ const pendingDocs = await pendingResponse.json();
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pagination": {
|
"count": 42,
|
||||||
"page": 1,
|
"currentPage": 1,
|
||||||
"perPage": 10,
|
"perPage": 10,
|
||||||
"totalPages": 5,
|
"totalPages": 5
|
||||||
"totalItems": 42
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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 Document
|
||||||
|
|
||||||
Delete a document. Completed documents cannot be deleted.
|
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();
|
const { success } = await response.json();
|
||||||
|
|
||||||
````
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
@@ -680,7 +791,7 @@ const { success } = await response.json();
|
|||||||
{
|
{
|
||||||
"success": true
|
"success": true
|
||||||
}
|
}
|
||||||
````
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -695,8 +806,10 @@ POST /envelope/get-many
|
|||||||
### Request Body
|
### Request Body
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| ------------- | ----- | -------- | --------------------- |
|
| ---------- | ------ | -------- | ---------------------------------------------------------------------------- |
|
||||||
| `envelopeIds` | array | Yes | Array of document IDs |
|
| `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
|
### Code Examples
|
||||||
|
|
||||||
@@ -707,12 +820,17 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/get-many" \
|
|||||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"envelopeIds": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
"ids": {
|
||||||
|
"type": "envelopeId",
|
||||||
|
"ids": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
||||||
|
}
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab value="TypeScript">
|
<Tab value="TypeScript">
|
||||||
```typescript
|
```typescript
|
||||||
|
const requestedIds = ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'];
|
||||||
|
|
||||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', {
|
const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -720,16 +838,36 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
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>
|
</Tab>
|
||||||
</Tabs>
|
</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
|
## Document Statuses
|
||||||
@@ -740,6 +878,7 @@ const documents = await response.json();
|
|||||||
| `PENDING` | Document has been sent. Waiting for recipients to sign. |
|
| `PENDING` | Document has been sent. Waiting for recipients to sign. |
|
||||||
| `COMPLETED` | All recipients have signed. Document is sealed. |
|
| `COMPLETED` | All recipients have signed. Document is sealed. |
|
||||||
| `REJECTED` | A recipient rejected the document. |
|
| `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
|
### Status Transitions
|
||||||
|
|
||||||
@@ -747,11 +886,13 @@ const documents = await response.json();
|
|||||||
flowchart LR
|
flowchart LR
|
||||||
DRAFT --> PENDING --> COMPLETED
|
DRAFT --> PENDING --> COMPLETED
|
||||||
PENDING --> REJECTED
|
PENDING --> REJECTED
|
||||||
|
PENDING --> CANCELLED
|
||||||
```
|
```
|
||||||
|
|
||||||
- **DRAFT to PENDING**: Call the distribute endpoint
|
- **DRAFT to PENDING**: Call the distribute endpoint
|
||||||
- **PENDING to COMPLETED**: All recipients complete their signing
|
- **PENDING to COMPLETED**: All recipients complete their signing
|
||||||
- **PENDING to REJECTED**: A recipient rejects the document
|
- **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">
|
<Callout type="warn">
|
||||||
You cannot modify recipients or fields after a document moves to `PENDING` status.
|
You cannot modify recipients or fields after a document moves to `PENDING` status.
|
||||||
@@ -773,8 +914,8 @@ flowchart LR
|
|||||||
| Parameter | Values | Description |
|
| Parameter | Values | Description |
|
||||||
| ---------- | ------------------------------------------- | ------------------------- |
|
| ---------- | ------------------------------------------- | ------------------------- |
|
||||||
| `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type |
|
| `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type |
|
||||||
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` | Filter by status |
|
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | Filter by status |
|
||||||
| `source` | `DOCUMENT`, `TEMPLATE`, `API` | Filter by creation source |
|
| `source` | `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | Filter by creation source |
|
||||||
| `folderId` | string | Filter by folder |
|
| `folderId` | string | Filter by folder |
|
||||||
|
|
||||||
### Sorting
|
### Sorting
|
||||||
@@ -800,10 +941,10 @@ async function getAllPendingDocuments() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, pagination } = await response.json();
|
const { data, currentPage, totalPages } = await response.json();
|
||||||
documents.push(...data);
|
documents.push(...data);
|
||||||
|
|
||||||
hasMore = page < pagination.totalPages;
|
hasMore = currentPage < totalPages;
|
||||||
page++;
|
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` | `GET /api/v2/envelope` |
|
||||||
| `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` |
|
| `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` | `POST /api/v2/envelope/create` |
|
||||||
| `POST /api/v2/document/create/beta` | `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` |
|
| `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` | `GET /api/v2/envelope` (with `type=TEMPLATE`) |
|
||||||
| `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` |
|
| `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` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||||
| `POST /api/v2/template/create/beta` | `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` |
|
| `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
|
## HTTP Rate Limits
|
||||||
|
|
||||||
|
The rate limit applies to:
|
||||||
|
|
||||||
|
- `/api/v1/*`
|
||||||
|
- `/api/v2/*`
|
||||||
|
- `/api/v2-beta/*`
|
||||||
|
|
||||||
**Limit:** 1000 requests per minute per IP address
|
**Limit:** 1000 requests per minute per IP address
|
||||||
**Response:** 429 Too Many Requests
|
**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.
|
this value, in which case you can be rate-limited before reaching the global limit.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
### Rate Limit Response
|
### Global per-IP 429 Response
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -27,10 +33,22 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<Callout type="warn">
|
### Rate Limit Headers
|
||||||
No rate limit headers are currently provided. When you receive a 429 response, wait at least 60
|
|
||||||
seconds before retrying.
|
Responses from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` include these headers. The only
|
||||||
</Callout>
|
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
|
## Resource Limits
|
||||||
|
|
||||||
@@ -44,25 +62,56 @@ Beyond HTTP rate limits, your account has usage limits based on your subscriptio
|
|||||||
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
|
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
|
||||||
| Direct Templates | 3 | 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
|
```json
|
||||||
{
|
{
|
||||||
"error": "You have reached your document limit for this month. Please upgrade your plan.",
|
"message": "Too many requests, please try again later. Contact support if you require higher limits."
|
||||||
"code": "LIMIT_EXCEEDED",
|
|
||||||
"statusCode": 400
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## Error Codes
|
||||||
|
|
||||||
| Code | Status | Description |
|
| Code | Status | Description |
|
||||||
| ------------------- | ------ | ----------------------------- |
|
| ------------------- | ------ | ------------------------------------------------------------------ |
|
||||||
| `TOO_MANY_REQUESTS` | 429 | HTTP rate limit exceeded |
|
| `TOO_MANY_REQUESTS` | 429 | Global per-IP, organisation windowed, or monthly quota exceeded |
|
||||||
| `LIMIT_EXCEEDED` | 400 | Resource usage limit 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|||||||
@@ -1000,9 +1000,12 @@ async function fetchWithRetry(
|
|||||||
// Retry on rate limit
|
// Retry on rate limit
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
const retryAfter = response.headers.get('Retry-After');
|
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...`);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ export const cancelEnvelopeMeta: TrpcRouteMeta = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/envelope/cancel',
|
path: '/envelope/cancel',
|
||||||
summary: 'Cancel envelope',
|
summary: 'Cancel envelope',
|
||||||
|
description: 'Cancel a pending envelope',
|
||||||
tags: ['Envelope'],
|
tags: ['Envelope'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCancelEnvelopeRequestSchema = z.object({
|
export const ZCancelEnvelopeRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string().describe('The ID of the envelope to cancel.'),
|
||||||
reason: z.string().optional(),
|
reason: z.string().describe('The reason for cancelling the envelope.').optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCancelEnvelopeResponseSchema = ZSuccessResponseSchema;
|
export const ZCancelEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ export const deleteEnvelopeMeta: TrpcRouteMeta = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/envelope/delete',
|
path: '/envelope/delete',
|
||||||
summary: 'Delete envelope',
|
summary: 'Delete envelope',
|
||||||
|
description: 'Delete an envelope',
|
||||||
tags: ['Envelope'],
|
tags: ['Envelope'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZDeleteEnvelopeRequestSchema = z.object({
|
export const ZDeleteEnvelopeRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string().describe('The ID of the envelope to delete.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
|
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||||
|
|||||||
@@ -12,24 +12,32 @@ export const updateEnvelopeMeta: TrpcRouteMeta = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/envelope/update',
|
path: '/envelope/update',
|
||||||
summary: 'Update envelope',
|
summary: 'Update envelope',
|
||||||
|
description: 'Update envelope properties and settings',
|
||||||
tags: ['Envelope'],
|
tags: ['Envelope'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZUpdateEnvelopeRequestSchema = z.object({
|
export const ZUpdateEnvelopeRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string().describe('The ID of the envelope to update.'),
|
||||||
data: z
|
data: z
|
||||||
.object({
|
.object({
|
||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
globalAccessAuth: z
|
||||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
.array(ZDocumentAccessAuthTypesSchema)
|
||||||
folderId: z.string().nullish(),
|
.describe('The authentication methods required to access the envelope.')
|
||||||
templateType: z.nativeEnum(TemplateType).optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
.optional(),
|
||||||
meta: ZDocumentMetaUpdateSchema.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.describe('The email and signing settings to update.').optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
|
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
|
||||||
|
|||||||
@@ -2,16 +2,27 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
import { SigningStatus } from '@prisma/client';
|
import { FieldType, SigningStatus } from '@prisma/client';
|
||||||
import { ClockIcon, EyeOffIcon, LockIcon } from 'lucide-react';
|
import {
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
CalendarDaysIcon,
|
||||||
|
CheckSquareIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ContactIcon,
|
||||||
|
DiscIcon,
|
||||||
|
EyeOffIcon,
|
||||||
|
HashIcon,
|
||||||
|
LockIcon,
|
||||||
|
MailIcon,
|
||||||
|
TypeIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { type ElementType, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { isTemplateRecipientEmailPlaceholder } from '../../../lib/constants/template';
|
import { isTemplateRecipientEmailPlaceholder } from '../../../lib/constants/template';
|
||||||
import { extractInitials } from '../../../lib/utils/recipient-formatter';
|
import { extractInitials } from '../../../lib/utils/recipient-formatter';
|
||||||
import { SignatureIcon } from '../../icons/signature';
|
import { SignatureIcon } from '../../icons/signature';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { Avatar, AvatarFallback } from '../../primitives/avatar';
|
import { Avatar, AvatarFallback } from '../../primitives/avatar';
|
||||||
import { Badge } from '../../primitives/badge';
|
|
||||||
import { FRIENDLY_FIELD_TYPE } from '../../primitives/document-flow/types';
|
import { FRIENDLY_FIELD_TYPE } from '../../primitives/document-flow/types';
|
||||||
import { PopoverHover } from '../../primitives/popover';
|
import { PopoverHover } from '../../primitives/popover';
|
||||||
|
|
||||||
@@ -27,16 +38,18 @@ interface EnvelopeRecipientFieldTooltipProps {
|
|||||||
showRecipientColors?: boolean;
|
showRecipientColors?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRecipientDisplayText = (recipient: { name: string; email: string }) => {
|
const FIELD_TYPE_ICONS: Record<FieldType, ElementType> = {
|
||||||
if (recipient.name && !isTemplateRecipientEmailPlaceholder(recipient.email)) {
|
[FieldType.SIGNATURE]: SignatureIcon,
|
||||||
return `${recipient.name} (${recipient.email})`;
|
[FieldType.FREE_SIGNATURE]: SignatureIcon,
|
||||||
}
|
[FieldType.INITIALS]: ContactIcon,
|
||||||
|
[FieldType.TEXT]: TypeIcon,
|
||||||
if (recipient.name && isTemplateRecipientEmailPlaceholder(recipient.email)) {
|
[FieldType.DATE]: CalendarDaysIcon,
|
||||||
return recipient.name;
|
[FieldType.EMAIL]: MailIcon,
|
||||||
}
|
[FieldType.NAME]: UserIcon,
|
||||||
|
[FieldType.NUMBER]: HashIcon,
|
||||||
return recipient.email;
|
[FieldType.RADIO]: DiscIcon,
|
||||||
|
[FieldType.CHECKBOX]: CheckSquareIcon,
|
||||||
|
[FieldType.DROPDOWN]: ChevronDownIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +63,8 @@ export function EnvelopeRecipientFieldTooltip({
|
|||||||
}: EnvelopeRecipientFieldTooltipProps) {
|
}: EnvelopeRecipientFieldTooltipProps) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const FieldIcon = FIELD_TYPE_ICONS[field.type];
|
||||||
|
|
||||||
const [hideField, setHideField] = useState<boolean>(!showRecipientTooltip);
|
const [hideField, setHideField] = useState<boolean>(!showRecipientTooltip);
|
||||||
|
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
@@ -138,54 +153,64 @@ export function EnvelopeRecipientFieldTooltip({
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
contentProps={{
|
contentProps={{
|
||||||
className: 'relative flex mb-4 w-fit flex-col p-4 text-sm',
|
className: 'flex w-64 flex-col overflow-hidden p-0 text-sm',
|
||||||
|
sideOffset: 20,
|
||||||
|
onOpenAutoFocus: (event) => event.preventDefault(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center gap-2 p-3">
|
||||||
|
<FieldIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
|
||||||
|
<p className="min-w-0 flex-1 truncate font-medium">
|
||||||
|
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
{showFieldStatus && (
|
{showFieldStatus && (
|
||||||
<Badge
|
<div className="flex shrink-0 items-center gap-1.5 text-xs">
|
||||||
className="mx-auto mb-1 py-0.5"
|
|
||||||
variant={
|
|
||||||
field?.fieldMeta?.readOnly
|
|
||||||
? 'neutral'
|
|
||||||
: field.recipient.signingStatus === SigningStatus.SIGNED
|
|
||||||
? 'default'
|
|
||||||
: 'secondary'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{field?.fieldMeta?.readOnly ? (
|
{field?.fieldMeta?.readOnly ? (
|
||||||
<>
|
<>
|
||||||
<LockIcon className="mr-1 h-3 w-3" />
|
<LockIcon className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
<Trans>Read Only</Trans>
|
<Trans>Read Only</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
) : field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||||
<>
|
<>
|
||||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
<span className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||||
|
<span className="text-green-600 dark:text-green-400">
|
||||||
<Trans>Signed</Trans>
|
<Trans>Signed</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ClockIcon className="mr-1 h-3 w-3" />
|
<span className="h-1.5 w-1.5 rounded-full bg-amber-400" />
|
||||||
|
<span className="text-amber-600 dark:text-amber-400">
|
||||||
<Trans>Pending</Trans>
|
<Trans>Pending</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-center font-semibold">
|
<div className="flex items-center gap-3 border-border/50 border-t bg-muted/50 px-3 py-2.5">
|
||||||
<span>
|
<div className="min-w-0 flex-1">
|
||||||
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
<p className="truncate font-medium text-xs">{field.recipient.name || field.recipient.email}</p>
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-1 text-center text-muted-foreground text-xs">{getRecipientDisplayText(field.recipient)}</p>
|
{!isTemplateRecipientEmailPlaceholder(field.recipient.email) && field.recipient.name && (
|
||||||
|
<p className="truncate text-muted-foreground text-xs">{field.recipient.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="absolute top-0 right-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
type="button"
|
||||||
|
className="-m-1 shrink-0 rounded-sm p-1 text-muted-foreground hover:bg-background hover:text-foreground"
|
||||||
onClick={() => setHideField(true)}
|
onClick={() => setHideField(true)}
|
||||||
title="Hide field"
|
title={t`Hide field`}
|
||||||
>
|
>
|
||||||
<EyeOffIcon className="h-3 w-3" />
|
<EyeOffIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</PopoverHover>
|
</PopoverHover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user