From 132c4b08c5d5f7697ec29de40df2e33afb86a9b3 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Wed, 16 Sep 2026 03:59:40 +0000 Subject: [PATCH] fix(trpc): v2 field position updates silently dropped; rewrite fields API docs (#3136) --- .../docs/developers/api/common-errors.mdx | 23 ++ .../content/docs/developers/api/fields.mdx | 281 +++++++++++------ .../content/docs/developers/api/index.mdx | 4 +- .../docs/developers/api/recipients.mdx | 20 +- .../content/docs/developers/api/teams.mdx | 71 ++--- .../content/docs/developers/api/templates.mdx | 287 ++++++++++++++++-- .../developers/examples/common-workflows.mdx | 94 ++++-- .../getting-started/authentication.mdx | 31 +- .../getting-started/first-api-call.mdx | 75 +++-- .../components/general/app-command-menu.tsx | 12 +- .../render-custom-email-template.test.ts | 12 +- .../envelope-fields/update-envelope-fields.ts | 12 +- 12 files changed, 661 insertions(+), 261 deletions(-) diff --git a/apps/docs/content/docs/developers/api/common-errors.mdx b/apps/docs/content/docs/developers/api/common-errors.mdx index f26eba13c..91fcb9313 100644 --- a/apps/docs/content/docs/developers/api/common-errors.mdx +++ b/apps/docs/content/docs/developers/api/common-errors.mdx @@ -15,6 +15,8 @@ This guide provides a comprehensive troubleshooting matrix for the standard erro | `INVALID_REQUEST` | The overall request is malformed or invalid. | Review your API call parameters, including the URL, query parameters, and headers. Correct the request syntax. | | `RECIPIENT_EXPIRED` | The signing link or recipient access has expired. | Generate and resend a new invitation to the affected recipient. | | `LIMIT_EXCEEDED` | Your account usage quota has been exceeded. | Check your current plan limits. Upgrade your subscription or wait until your billing cycle renews. | +| `MISSING_ENV_VAR` | A required environment variable is not configured on the server (500). | Primarily affects self-hosted instances: set the environment variable named in the error message and restart. On Documenso Cloud, contact support. | +| `MISSING_SIGNATURE_FIELD` | A signer has no signature field placed on the document (400). Returned when distributing an envelope. | Add at least one signature field for every recipient with a signing role before calling `/envelope/distribute`. | | `NOT_FOUND` | The requested resource could not be found (404). | Verify the resource ID (envelope, document, webhook) passed in the URL. Ensure the resource has not been deleted. | | `NOT_IMPLEMENTED` | The requested feature is not currently supported by the server. | Consult the API documentation to verify available methods. Do not use this endpoint at this time. | | `NOT_SETUP` | The required configuration for this action is incomplete. | Access your account or integration settings and complete the necessary configuration before retrying. | @@ -37,7 +39,28 @@ The following errors occur when attempting to perform actions on an envelope tha | `ENVELOPE_DRAFT` | The action cannot be performed because the envelope is still in a draft state. | Finalize the envelope configuration and transition it to the `PENDING` (sent) state before attempting this operation. | | `ENVELOPE_COMPLETED` | The action cannot be performed because the envelope is already completed. | No further modifications (e.g., adding signers, modifying documents) can be made to an envelope once the signing process is finished. | | `ENVELOPE_REJECTED` | The action cannot be performed because the envelope was rejected by a recipient. | The signing flow is permanently halted. Create a new envelope if you wish to resubmit the document. | +| `ENVELOPE_CANCELLED` | The action cannot be performed because the envelope was cancelled (400). | Create a new envelope if you need to restart the signing process. | | `ENVELOPE_LEGACY` | The action cannot be performed because the envelope uses an obsolete format. | This envelope was created with a legacy version of the system. Recreate the envelope using the current API version to interact with it. | +| `ENVELOPE_TSP_LOCKED` | An AES/QES envelope cannot be modified after it leaves the draft state (400). | Make changes while the envelope is in `DRAFT`, or create a new envelope. | + +## CSC Signing Errors + +These errors apply to Cloud Signature Consortium (CSC) signing flows. + +| Error Code | Description | Recommended Action | +| :--- | :--- | :--- | +| `CSC_INSTANCE_MODE_MISMATCH` | The requested signature level does not match the instance's CSC mode (400). | Use the signature level supported by the instance's signing configuration. | +| `CSC_UNLICENSED` | CSC signing is not licensed for this instance (403). | Enable the CSC signing license before retrying. | +| `CSC_PROVIDER_INFO_FAILED` | The CSC provider's discovery request failed or returned unusable information (500). | Check the provider URL, availability, and OAuth configuration. | +| `CSC_PROVIDER_NO_TSA` | A timestamp authority is unavailable or unusable for CSC signing (500). | Configure `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` and verify provider timestamp access. | +| `CSC_CREDENTIAL_LIST_EMPTY` | The CSC provider returned no signing credentials for the authenticated user (400). | Enrol a signing credential with the provider, then authenticate again. | +| `CSC_CERT_INVALID` | The selected signing certificate is missing, invalid, or outside its validity period (400). | Select or renew a valid certificate, then authenticate again. | +| `CSC_ALGORITHM_REFUSED` | The signing credential uses an unsupported key or digest algorithm (400). | Select a credential that satisfies the instance's CSC algorithm policy. | +| `CSC_SAD_EXPIRED_PRE_SIGN` | The signature activation data is missing, expired, or unreadable before signing (400). | Repeat the credential authorization flow. | +| `CSC_TSP_TIMEOUT` | The trust service provider did not complete the signing request before the timeout (408). | Retry the signing request after checking provider availability. | +| `CSC_EMBED_FAILED` | The returned CSC signature could not be embedded into the envelope items (400). | Restart the signing attempt. If it fails again, contact support. | +| `CSC_BASE_DOCUMENT_MUTATED` | The document changed between signature preparation and signing (500). | Restart signing from the current envelope state. | +| `CSC_REQUEST_FAILED` | A CSC provider request failed without a more specific CSC error (500). | Check provider availability and configuration, then retry. | ## See Also diff --git a/apps/docs/content/docs/developers/api/fields.mdx b/apps/docs/content/docs/developers/api/fields.mdx index 71f680a62..0cec72339 100644 --- a/apps/docs/content/docs/developers/api/fields.mdx +++ b/apps/docs/content/docs/developers/api/fields.mdx @@ -6,6 +6,8 @@ description: Add signature and form fields to documents via API. import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). @@ -19,13 +21,13 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; | `secondaryId` | string | Secondary identifier for audit logs | | `type` | string | Field type (see [Field Types](#field-types)) | | `recipientId` | number | ID of the recipient assigned to this field | -| `envelopeId` | number | ID of the parent envelope | +| `envelopeId` | string | ID of the parent envelope | | `envelopeItemId` | string | ID of the PDF item the field is placed on | | `page` | number | Page number (1-indexed) | -| `positionX` | number | X coordinate as percentage (0-100) | -| `positionY` | number | Y coordinate as percentage (0-100) | -| `width` | number | Width as percentage of page (0-100) | -| `height` | number | Height as percentage of page (0-100) | +| `positionX` | string | X coordinate as percentage (0-100), a decimal serialized as a string | +| `positionY` | string | Y coordinate as percentage (0-100), a decimal serialized as a string | +| `width` | string | Width as percentage of page (0-100), a decimal serialized as a string | +| `height` | string | Height as percentage of page (0-100), a decimal serialized as a string | | `customText` | string | Value entered by the recipient | | `inserted` | boolean | Whether the field has been completed | | `fieldMeta` | object \| null | Type-specific configuration options | @@ -38,18 +40,19 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; "secondaryId": "field_abc123", "type": "SIGNATURE", "recipientId": 123, - "envelopeId": 789, - "envelopeItemId": "envelope_item_xyz", + "envelopeId": "envelope_abcdefhiklmnorst", + "envelopeItemId": "envelope_item_abcdefhiklmnorst", "page": 1, - "positionX": 10, - "positionY": 80, - "width": 30, - "height": 5, + "positionX": "10", + "positionY": "80", + "width": "30", + "height": "5", "customText": "", "inserted": false, "fieldMeta": { "type": "signature", - "required": true + "required": true, + "overflow": "auto" } } ``` @@ -61,7 +64,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; | Type | Description | Auto-filled | | ---------------- | ----------------------------------------- | ----------- | | `SIGNATURE` | Drawn, typed, or uploaded signature | No | -| `FREE_SIGNATURE` | Unrestricted signature without validation | No | +| `FREE_SIGNATURE` | Legacy free-form signature. Accepted by the v2 create schema but rejected by the v1 API and unsupported in the signing UI — avoid in new integrations | No | | `INITIALS` | Recipient's initials | No | | `NAME` | Recipient's full name | Yes | | `EMAIL` | Recipient's email address | Yes | @@ -134,10 +137,12 @@ POST /envelope/field/create-many ### Request Body -| Field | Type | Required | Description | -| ----------- | ------ | -------- | ------------------------------- | -| `documentId`| number | Yes | The document ID | -| `fields` | array | Yes | Array of field configurations | +| Field | Type | Required | Description | +| ------------ | ------ | -------- | ------------------------------- | +| `envelopeId` | string | Yes | The envelope ID | +| `data` | array | Yes | Array of field configurations | + +Each entry in `data` requires a `type`, a `recipientId`, and a position — either explicit coordinates (`page`, `positionX`, `positionY`, `width`, `height`) or a [text placeholder](#placeholder-based-field-positioning) (`placeholder` with optional `width`, `height`, and `matchAll`). Optional per-entry properties: `envelopeItemId` (which PDF in the envelope to place the field on; defaults to the first item) and `fieldMeta`. ### Code Examples @@ -148,32 +153,32 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/field/create-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ - "documentId": 123, - "fields": [ + "envelopeId": "envelope_abcdefhiklmnorst", + "data": [ { "type": "SIGNATURE", "recipientId": 456, - "pageNumber": 1, - "pageX": 10, - "pageY": 80, + "page": 1, + "positionX": 10, + "positionY": 80, "width": 30, "height": 5 }, { "type": "DATE", "recipientId": 456, - "pageNumber": 1, - "pageX": 50, - "pageY": 80, + "page": 1, + "positionX": 50, + "positionY": 80, "width": 20, "height": 3 }, { "type": "TEXT", "recipientId": 456, - "pageNumber": 1, - "pageX": 10, - "pageY": 70, + "page": 1, + "positionX": 10, + "positionY": 70, "width": 40, "height": 4, "fieldMeta": { @@ -199,32 +204,32 @@ const response = await fetch( 'Content-Type': 'application/json', }, body: JSON.stringify({ - documentId: 123, - fields: [ + envelopeId: 'envelope_abcdefhiklmnorst', + data: [ { type: 'SIGNATURE', recipientId: 456, - pageNumber: 1, - pageX: 10, - pageY: 80, + page: 1, + positionX: 10, + positionY: 80, width: 30, height: 5, }, { type: 'DATE', recipientId: 456, - pageNumber: 1, - pageX: 50, - pageY: 80, + page: 1, + positionX: 50, + positionY: 80, width: 20, height: 3, }, { type: 'TEXT', recipientId: 456, - pageNumber: 1, - pageX: 10, - pageY: 70, + page: 1, + positionX: 10, + positionY: 70, width: 40, height: 4, fieldMeta: { @@ -239,8 +244,8 @@ const response = await fetch( } ); -const { fields } = await response.json(); -console.log(`Created ${fields.length} fields`); +const { data } = await response.json(); +console.log(`Created ${data.length} fields`); ```` @@ -250,36 +255,68 @@ console.log(`Created ${fields.length} fields`); ```json { - "fields": [ + "data": [ { "id": 101, + "secondaryId": "field_abc123", + "envelopeId": "envelope_abcdefhiklmnorst", + "envelopeItemId": "envelope_item_abcdefhiklmnorst", "type": "SIGNATURE", "recipientId": 456, "page": 1, - "positionX": 10, - "positionY": 80, - "width": 30, - "height": 5 + "positionX": "10", + "positionY": "80", + "width": "30", + "height": "5", + "customText": "", + "inserted": false, + "fieldMeta": { + "type": "signature", + "fontSize": 18, + "overflow": "auto" + } }, { "id": 102, + "secondaryId": "field_def456", + "envelopeId": "envelope_abcdefhiklmnorst", + "envelopeItemId": "envelope_item_abcdefhiklmnorst", "type": "DATE", "recipientId": 456, "page": 1, - "positionX": 50, - "positionY": 80, - "width": 20, - "height": 3 + "positionX": "50", + "positionY": "80", + "width": "20", + "height": "3", + "customText": "", + "inserted": false, + "fieldMeta": { + "type": "date", + "fontSize": 12, + "textAlign": "left", + "overflow": "auto" + } }, { "id": 103, + "secondaryId": "field_ghi789", + "envelopeId": "envelope_abcdefhiklmnorst", + "envelopeItemId": "envelope_item_abcdefhiklmnorst", "type": "TEXT", "recipientId": 456, "page": 1, - "positionX": 10, - "positionY": 70, - "width": 40, - "height": 4 + "positionX": "10", + "positionY": "70", + "width": "40", + "height": "4", + "customText": "", + "inserted": false, + "fieldMeta": { + "type": "text", + "label": "Job Title", + "placeholder": "Enter your job title", + "required": true + } } ] } @@ -299,8 +336,10 @@ POST /envelope/field/update-many | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | -| `documentId` | number | Yes | The document ID | -| `fields` | array | Yes | Array of field update objects | +| `envelopeId` | string | Yes | The envelope ID | +| `data` | array | Yes | Array of field update objects | + +Each entry in `data` requires the field `id` and `type`. Position properties (`page`, `positionX`, `positionY`, `width`, `height`), `envelopeItemId`, and `fieldMeta` are optional — only supplied values are updated. Placeholder positioning is not supported when updating; use coordinates. ### Code Examples @@ -311,17 +350,17 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/field/update-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ - "documentId": 123, - "fields": [ + "envelopeId": "envelope_abcdefhiklmnorst", + "data": [ { "id": 101, "type": "SIGNATURE", - "pageY": 85 + "positionY": 85 }, { "id": 102, "type": "DATE", - "pageY": 85 + "positionY": 85 } ] }' @@ -338,16 +377,16 @@ const response = await fetch( 'Content-Type': 'application/json', }, body: JSON.stringify({ - documentId: 123, - fields: [ - { id: 101, type: 'SIGNATURE', pageY: 85 }, - { id: 102, type: 'DATE', pageY: 85 }, + envelopeId: 'envelope_abcdefhiklmnorst', + data: [ + { id: 101, type: 'SIGNATURE', positionY: 85 }, + { id: 102, type: 'DATE', positionY: 85 }, ], }), } ); -const { fields } = await response.json(); +const { data } = await response.json(); ```` @@ -357,9 +396,48 @@ const { fields } = await response.json(); ```json { - "fields": [ - { "id": 101, "type": "SIGNATURE", "positionY": 85 }, - { "id": 102, "type": "DATE", "positionY": 85 } + "data": [ + { + "id": 101, + "secondaryId": "field_abc123", + "envelopeId": "envelope_abcdefhiklmnorst", + "envelopeItemId": "envelope_item_abcdefhiklmnorst", + "type": "SIGNATURE", + "recipientId": 456, + "page": 1, + "positionX": "10", + "positionY": "85", + "width": "30", + "height": "5", + "customText": "", + "inserted": false, + "fieldMeta": { + "type": "signature", + "fontSize": 18, + "overflow": "auto" + } + }, + { + "id": 102, + "secondaryId": "field_def456", + "envelopeId": "envelope_abcdefhiklmnorst", + "envelopeItemId": "envelope_item_abcdefhiklmnorst", + "type": "DATE", + "recipientId": 456, + "page": 1, + "positionX": "50", + "positionY": "85", + "width": "20", + "height": "3", + "customText": "", + "inserted": false, + "fieldMeta": { + "type": "date", + "fontSize": 12, + "textAlign": "left", + "overflow": "auto" + } + } ] } ```` @@ -443,8 +521,8 @@ Fields use percentage-based coordinates relative to the PDF page dimensions. (0,0) ─────────────────────────── (100,0) │ │ │ ┌─────────┐ │ - │ │ Field │ (pageX: 10, │ - │ │ │ pageY: 20, │ + │ │ Field │ (positionX: 10, │ + │ │ │ positionY: 20, │ │ └─────────┘ width: 30, │ │ height: 5) │ │ │ @@ -457,9 +535,9 @@ Fields use percentage-based coordinates relative to the PDF page dimensions. const field = { type: 'SIGNATURE', recipientId: 123, - pageNumber: 1, - pageX: 60, // 60% from left - pageY: 85, // 85% from top (near bottom) + page: 1, + positionX: 60, // 60% from left + positionY: 85, // 85% from top (near bottom) width: 30, // 30% of page width height: 8, // 8% of page height }; @@ -479,6 +557,33 @@ This approach is useful when generating PDFs programmatically or using templates See the [PDF Placeholders](/docs/users/documents/advanced/pdf-placeholders) guide for the full placeholder format reference, including supported field types, recipient identifiers, and field options. +### Placeholder Positioning via the API + +`POST /envelope/field/create-many` accepts a placeholder position in place of coordinates. Instead of `page`, `positionX`, `positionY`, `width`, and `height`, pass: + +| Field | Type | Required | Description | +| ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------ | +| `placeholder` | string | Yes | Text to search for in the PDF (e.g. `{{name}}`). The field is placed at the bounding box of the first match. | +| `width` | number | No | Override the field width. Defaults to the width of the matched text. | +| `height` | number | No | Override the field height. Defaults to the height of the matched text. | +| `matchAll` | boolean | No | Create a field at every occurrence of the placeholder instead of only the first. | + +```json +{ + "envelopeId": "envelope_abcdefhiklmnorst", + "data": [ + { + "type": "SIGNATURE", + "recipientId": 456, + "placeholder": "{{signature}}", + "matchAll": true + } + ] +} +``` + +`POST /envelope/field/update-many` does not accept placeholders — field updates are coordinate-only. + --- ## Field Meta Options @@ -643,15 +748,15 @@ All field types support these base options: Create a document with a signature block containing multiple field types: ```typescript -async function addSignatureBlock(documentId: number, recipientId: number) { - const fields = [ +async function addSignatureBlock(envelopeId: string, recipientId: number) { + const data = [ // Signature { type: 'SIGNATURE', recipientId, - pageNumber: 1, - pageX: 10, - pageY: 80, + page: 1, + positionX: 10, + positionY: 80, width: 30, height: 8, fieldMeta: { @@ -663,9 +768,9 @@ async function addSignatureBlock(documentId: number, recipientId: number) { { type: 'NAME', recipientId, - pageNumber: 1, - pageX: 10, - pageY: 90, + page: 1, + positionX: 10, + positionY: 90, width: 30, height: 4, fieldMeta: { @@ -677,9 +782,9 @@ async function addSignatureBlock(documentId: number, recipientId: number) { { type: 'DATE', recipientId, - pageNumber: 1, - pageX: 50, - pageY: 80, + page: 1, + positionX: 50, + positionY: 80, width: 20, height: 4, fieldMeta: { @@ -691,9 +796,9 @@ async function addSignatureBlock(documentId: number, recipientId: number) { { type: 'TEXT', recipientId, - pageNumber: 1, - pageX: 50, - pageY: 90, + page: 1, + positionX: 50, + positionY: 90, width: 30, height: 4, fieldMeta: { @@ -710,7 +815,7 @@ async function addSignatureBlock(documentId: number, recipientId: number) { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, - body: JSON.stringify({ documentId, fields }), + body: JSON.stringify({ envelopeId, data }), }); return response.json(); diff --git a/apps/docs/content/docs/developers/api/index.mdx b/apps/docs/content/docs/developers/api/index.mdx index e8d7139eb..ac64de966 100644 --- a/apps/docs/content/docs/developers/api/index.mdx +++ b/apps/docs/content/docs/developers/api/index.mdx @@ -60,8 +60,8 @@ Authorization: api_xxxxxxxxxxxxxxxx href="/docs/developers/api/templates" /> diff --git a/apps/docs/content/docs/developers/api/recipients.mdx b/apps/docs/content/docs/developers/api/recipients.mdx index 6751a400a..bb72afb92 100644 --- a/apps/docs/content/docs/developers/api/recipients.mdx +++ b/apps/docs/content/docs/developers/api/recipients.mdx @@ -6,6 +6,8 @@ description: Add and manage envelope recipients via API. import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). @@ -16,7 +18,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; ```json { "id": 123, - "envelopeId": "clu1abc2def3ghi4jkl", + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -134,7 +136,7 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/create-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ - "envelopeId": "clu1abc2def3ghi4jkl", + "envelopeId": "envelope_abcdefhiklmnorst", "data": [ { "email": "signer@example.com", @@ -164,7 +166,7 @@ const response = await fetch( 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeId: 'clu1abc2def3ghi4jkl', + envelopeId: 'envelope_abcdefhiklmnorst', data: [ { email: 'signer@example.com', @@ -196,7 +198,7 @@ const { data: recipients } = await response.json(); "data": [ { "id": 789, - "envelopeId": "clu1abc2def3ghi4jkl", + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -209,7 +211,7 @@ const { data: recipients } = await response.json(); }, { "id": 790, - "envelopeId": "clu1abc2def3ghi4jkl", + "envelopeId": "envelope_abcdefhiklmnorst", "email": "approver@example.com", "name": "Jane Smith", "role": "APPROVER", @@ -262,7 +264,7 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/update-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ - "envelopeId": "clu1abc2def3ghi4jkl", + "envelopeId": "envelope_abcdefhiklmnorst", "data": [ { "id": 789, @@ -284,7 +286,7 @@ const response = await fetch( 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeId: 'clu1abc2def3ghi4jkl', + envelopeId: 'envelope_abcdefhiklmnorst', data: [ { id: 789, @@ -387,7 +389,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/recipien 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeId: 'clu1abc2def3ghi4jkl', + envelopeId: 'envelope_abcdefhiklmnorst', data: [ { email: 'approver@example.com', @@ -462,7 +464,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/recipien 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeId: 'clu1abc2def3ghi4jkl', + envelopeId: 'envelope_abcdefhiklmnorst', data: [ { email: 'signer@example.com', diff --git a/apps/docs/content/docs/developers/api/teams.mdx b/apps/docs/content/docs/developers/api/teams.mdx index 0d869c56c..258e015c0 100644 --- a/apps/docs/content/docs/developers/api/teams.mdx +++ b/apps/docs/content/docs/developers/api/teams.mdx @@ -1,44 +1,29 @@ --- -title: Teams API -description: Manage team resources, documents, and templates with team-scoped API tokens. +title: Team-Scoped API Access +description: Use team-scoped API tokens with document and template envelopes. --- import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). -## Team Object +## Team Context -A team object contains the following properties: + + The V2 REST API does not expose `/team/*` endpoints. Create and manage teams, members, and team + settings in the Documenso web application. This page explains how a team-scoped token applies + that team context to supported API resources. + -| Property | Type | Description | -| ----------------- | -------------- | --------------------------------------------------- | -| `id` | number | Unique team identifier | -| `name` | string | Team display name | -| `url` | string | Unique team URL slug | -| `createdAt` | string | ISO 8601 timestamp | -| `avatarImageId` | string \| null | ID of the team's avatar image | -| `organisationId` | string | ID of the parent organisation | -| `currentTeamRole` | string | Your role in the team: `ADMIN`, `MANAGER`, `MEMBER` | - -### Example Team Object - -```json -{ - "id": 123, - "name": "Engineering", - "url": "engineering", - "createdAt": "2025-01-15T10:30:00.000Z", - "avatarImageId": null, - "organisationId": "org_abc123", - "currentTeamRole": "ADMIN" -} -``` +The API resolves the team from your token. You do not pass a team ID when creating, listing, or +using envelopes. The token's team ID determines which resources the request can access. ## Team-Scoped API Tokens @@ -156,26 +141,26 @@ Retrieve all documents belonging to the team: ```bash # List all team documents -curl -X GET "https://app.documenso.com/api/v2/envelope" \ +curl -X GET "https://app.documenso.com/api/v2/envelope?type=DOCUMENT" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" # Filter by status -curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING" \ +curl -X GET "https://app.documenso.com/api/v2/envelope?type=DOCUMENT&status=PENDING" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ```` ```typescript -const response = await fetch('https://app.documenso.com/api/v2/envelope', { +const response = await fetch('https://app.documenso.com/api/v2/envelope?type=DOCUMENT', { method: 'GET', headers: { Authorization: TEAM_API_TOKEN, }, }); -const { data, pagination } = await response.json(); -console.log(`Found ${pagination.totalItems} team documents`); +const { data, count } = await response.json(); +console.log(`Found ${count} team documents`); ```` @@ -190,10 +175,11 @@ Templates created with a team token are shared across the team. ```bash -curl -X POST "https://app.documenso.com/api/v2/template/create" \ +curl -X POST "https://app.documenso.com/api/v2/envelope/create" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: multipart/form-data" \ -F 'payload={ + "type": "TEMPLATE", "title": "NDA Template", "recipients": [ { @@ -223,6 +209,7 @@ curl -X POST "https://app.documenso.com/api/v2/template/create" \ const form = new FormData(); const payload = { + type: 'TEMPLATE', title: 'NDA Template', recipients: [ { @@ -249,7 +236,7 @@ form.append('files', fs.createReadStream('./nda-template.pdf'), { contentType: 'application/pdf', }); -const response = await fetch('https://app.documenso.com/api/v2/template/create', { +const response = await fetch('https://app.documenso.com/api/v2/envelope/create', { method: 'POST', headers: { Authorization: TEAM_API_TOKEN, @@ -257,8 +244,8 @@ const response = await fetch('https://app.documenso.com/api/v2/template/create', body: form, }); -const template = await response.json(); -console.log('Created team template:', template.id); +const { id } = await response.json(); +console.log('Created team template envelope:', id); ```` @@ -268,14 +255,14 @@ console.log('Created team template:', template.id); ```bash -curl -X GET "https://app.documenso.com/api/v2/template" \ +curl -X GET "https://app.documenso.com/api/v2/envelope?type=TEMPLATE" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ```` ```typescript -const response = await fetch('https://app.documenso.com/api/v2/template', { +const response = await fetch('https://app.documenso.com/api/v2/envelope?type=TEMPLATE', { method: 'GET', headers: { Authorization: TEAM_API_TOKEN, @@ -330,19 +317,19 @@ const SALES_TEAM_TOKEN = process.env.SALES_TEAM_API_TOKEN; const LEGAL_TEAM_TOKEN = process.env.LEGAL_TEAM_API_TOKEN; // Get pending documents from sales team -const salesResponse = await fetch('https://app.documenso.com/api/v2/envelope?status=PENDING', { +const salesResponse = await fetch('https://app.documenso.com/api/v2/envelope?type=DOCUMENT&status=PENDING', { headers: { Authorization: SALES_TEAM_TOKEN }, }); const salesDocs = await salesResponse.json(); // Get completed documents from legal team -const legalResponse = await fetch('https://app.documenso.com/api/v2/envelope?status=COMPLETED', { +const legalResponse = await fetch('https://app.documenso.com/api/v2/envelope?type=DOCUMENT&status=COMPLETED', { headers: { Authorization: LEGAL_TEAM_TOKEN }, }); const legalDocs = await legalResponse.json(); -console.log(`Sales team: ${salesDocs.pagination.totalItems} pending`); -console.log(`Legal team: ${legalDocs.pagination.totalItems} completed`); +console.log(`Sales team: ${salesDocs.count} pending`); +console.log(`Legal team: ${legalDocs.count} completed`); ``` ## Error Responses diff --git a/apps/docs/content/docs/developers/api/templates.mdx b/apps/docs/content/docs/developers/api/templates.mdx index b3f52e146..2d3675100 100644 --- a/apps/docs/content/docs/developers/api/templates.mdx +++ b/apps/docs/content/docs/developers/api/templates.mdx @@ -13,7 +13,194 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; see the [OpenAPI Reference](https://openapi.documenso.com). -## Template Object +## Use a Template Envelope + +New integrations should create a document from a template envelope with the Envelope API. + +``` +POST /envelope/use +Content-Type: multipart/form-data +``` + +The request uses `multipart/form-data`: + +| Part | Type | Required | Description | +| --------- | ------- | -------- | ------------------------------------------------------------------ | +| `payload` | JSON | Yes | Template envelope ID, recipient details, and document settings | +| `files` | File(s) | No | Replacement PDFs referenced by entries in `customDocumentData` | + +### Payload Schema + +| Field | Type | Required | Description | +| -------------------- | ------- | -------- | ------------------------------------------------------------------------ | +| `envelopeId` | string | Yes | ID of the template envelope | +| `externalId` | string | No | Your identifier for the created document envelope | +| `recipients` | array | No | Recipient details mapped to recipients in the template | +| `distributeDocument` | boolean | No | If `true`, create the document as pending and distribute it | +| `customDocumentData` | array | No | Maps uploaded replacement PDFs to template envelope items | +| `folderId` | string | No | Folder in which to create the document | +| `prefillFields` | array | No | Field values to prefill before distribution | +| `override` | object | No | Template values to override for the created document | +| `attachments` | array | No | Link attachments to add to the document | +| `formValues` | object | No | PDF form values to apply | + +Each recipient entry accepts the following fields: + +| Field | Type | Required | Description | +| -------------- | ------- | -------- | -------------------------------------------- | +| `id` | number | Yes | Recipient ID from the template envelope | +| `email` | string | Yes | Recipient email address | +| `name` | string | No | Recipient display name | +| `signingOrder` | number | No | Recipient position in sequential signing | + +Each `customDocumentData` entry maps an uploaded file to a template item: + +| Field | Type | Required | Description | +| ---------------- | ---------------- | -------- | --------------------------------------------------------------- | +| `identifier` | string \| number | Yes | Uploaded filename or zero-based file index | +| `envelopeItemId` | string | Yes | Template envelope item whose PDF the uploaded file replaces | + +### Code Examples + + + +```bash +curl -X POST "https://app.documenso.com/api/v2/envelope/use" \ + -H "Authorization: api_xxxxxxxxxxxxxxxx" \ + -F 'payload={ + "envelopeId": "envelope_template123", + "externalId": "contract-2025-001", + "recipients": [ + { + "id": 1, + "email": "john.doe@example.com", + "name": "John Doe" + } + ], + "prefillFields": [ + { + "id": 101, + "type": "text", + "value": "Senior Software Engineer" + } + ], + "distributeDocument": false + }' +``` + + +```typescript +const form = new FormData(); + +form.append( + 'payload', + JSON.stringify({ + envelopeId: 'envelope_template123', + externalId: 'contract-2025-001', + recipients: [ + { + id: 1, + email: 'john.doe@example.com', + name: 'John Doe', + }, + ], + prefillFields: [ + { + id: 101, + type: 'text', + value: 'Senior Software Engineer', + }, + ], + distributeDocument: false, + }), +); + +const response = await fetch('https://app.documenso.com/api/v2/envelope/use', { + method: 'POST', + headers: { + Authorization: 'api_xxxxxxxxxxxxxxxx', + }, + body: form, +}); + +const document = await response.json(); +console.log('Created document envelope:', document.id); +``` + + + +### Response + +```json +{ + "id": "envelope_document123", + "recipients": [ + { + "id": 1, + "name": "John Doe", + "email": "john.doe@example.com", + "token": "recipient_token", + "role": "SIGNER", + "signingOrder": 1, + "signingUrl": "https://app.documenso.com/sign/recipient_token" + } + ] +} +``` + +### Distribute the Created Envelope + +If you leave `distributeDocument` unset or set it to `false`, distribute the created document with +`POST /envelope/distribute`. Its response confirms delivery and includes each recipient's signing URL. + +```typescript +const distributionResponse = await fetch( + 'https://app.documenso.com/api/v2/envelope/distribute', + { + method: 'POST', + headers: { + Authorization: 'api_xxxxxxxxxxxxxxxx', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + envelopeId: document.id, + }), + }, +); + +const distribution = await distributionResponse.json(); +console.log('Signing URL:', distribution.recipients[0].signingUrl); +``` + +```json +{ + "success": true, + "id": "envelope_document123", + "recipients": [ + { + "id": 1, + "name": "John Doe", + "email": "john.doe@example.com", + "token": "recipient_token", + "role": "SIGNER", + "signingOrder": 1, + "signingUrl": "https://app.documenso.com/sign/recipient_token" + } + ] +} +``` + +--- + +## Deprecated Template Endpoint Reference + + + Every `/template/*` endpoint below is deprecated. Use the Envelope API for new integrations and + follow [Migrating to Envelopes](/docs/developers/api/migrate-to-envelopes) to replace existing calls. + The legacy reference remains here to support migrations. + + +## Legacy Template Object A template object contains the following properties: @@ -91,7 +278,7 @@ A template object contains the following properties: } ``` -## List Templates +## List Templates (Deprecated) Retrieve a paginated list of templates. @@ -139,8 +326,8 @@ const response = await fetch(`${BASE_URL}/template`, { }, }); -const { data, pagination } = await response.json(); -console.log(`Found ${pagination.totalItems} templates`); +const { data, count } = await response.json(); +console.log(`Found ${count} templates`); // Filter by type const privateResponse = await fetch( @@ -181,18 +368,16 @@ const privateTemplates = await privateResponse.json(); ] } ], - "pagination": { - "page": 1, - "perPage": 10, - "totalPages": 3, - "totalItems": 25 - } + "count": 25, + "currentPage": 1, + "perPage": 10, + "totalPages": 3 } ``` --- -## Get Template +## Get Template (Deprecated) Retrieve a single template by ID. @@ -238,9 +423,9 @@ Returns the full template object including recipients, fields, and metadata. --- -## Create Document from Template +## Create Document from Template (Deprecated) -Create a new document using a template. This is the primary way to use templates programmatically. +Create a new document using the deprecated template endpoint. This endpoint does not support [PDF placeholder parsing](/docs/users/documents/advanced/pdf-placeholders). Use `POST /envelope/create` for placeholder-based field positioning. @@ -415,32 +600,57 @@ const prefilledDocument = await prefillResponse.json(); ### Response -Returns the created document object with recipients and signing URLs. +The endpoint returns the full legacy document object. The selected fields below show both the numeric +legacy `id` and canonical `envelopeId`. Recipient entries do not include a `signingUrl`. ```json { - "id": "envelope_xyz789", - "type": "DOCUMENT", + "id": 789, + "envelopeId": "envelope_xyz789", "status": "PENDING", - "title": "Employment Contract", "source": "TEMPLATE", + "title": "Employment Contract", "externalId": "contract-2025-001", "recipients": [ { "id": 1, + "envelopeId": "envelope_xyz789", + "documentId": 789, + "templateId": null, "email": "john.doe@example.com", "name": "John Doe", "role": "SIGNER", "signingStatus": "NOT_SIGNED", - "signingUrl": "https://app.documenso.com/sign/abc123" + "signingOrder": 1 } ] } -```` +``` + +To send a document created with `distributeDocument: false` and receive signing links, call +`POST /envelope/distribute` with its `envelopeId`: + +```typescript +const document = await response.json(); + +const distributionResponse = await fetch(`${BASE_URL}/envelope/distribute`, { + method: 'POST', + headers: { + Authorization: API_TOKEN, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + envelopeId: document.envelopeId, + }), +}); + +const distribution = await distributionResponse.json(); +console.log('Signing URL:', distribution.recipients[0].signingUrl); +``` --- -## Override Template Settings +## Override Template Settings (Deprecated) When creating a document from a template, you can override various settings: @@ -488,7 +698,7 @@ const response = await fetch(`${BASE_URL}/template/use`, { --- -## Prefill Fields +## Prefill Fields (Deprecated) Prefill field values when creating a document from a template. This is useful for populating known data before sending. @@ -577,7 +787,7 @@ const response = await fetch(`${BASE_URL}/template/use`, { --- -## Update Template +## Update Template (Deprecated) Update a template's properties. @@ -643,7 +853,7 @@ const template = await response.json(); --- -## Duplicate Template +## Duplicate Template (Deprecated) Create a copy of an existing template. @@ -695,7 +905,7 @@ console.log('New template ID:', duplicatedTemplate.id); --- -## Delete Template +## Delete Template (Deprecated) Delete a template. @@ -754,7 +964,7 @@ const { success } = await response.json(); --- -## Direct Link Templates +## Direct Link Templates (Deprecated) Direct link templates allow recipients to create and sign documents without requiring you to explicitly create each document. When a recipient visits the direct link, a new document is automatically created from the template. @@ -898,7 +1108,7 @@ const { success } = await response.json(); --- -## Custom Document Data +## Custom Document Data (Deprecated) When creating a document from a template, you can replace the template's PDF with a custom PDF by using the `customDocumentData` parameter. This is useful when you need to generate the PDF dynamically while reusing the template's recipient and field configuration. @@ -913,7 +1123,7 @@ See the [OpenAPI Reference](https://openapi.documenso.com) for the full request --- -## Template Types +## Template Types (Legacy) | Type | Description | | --------- | ------------------------------------------------------------------ | @@ -922,7 +1132,7 @@ See the [OpenAPI Reference](https://openapi.documenso.com) for the full request --- -## Complete Example: Contract Workflow +## Complete Legacy Example: Contract Workflow (Deprecated) This example demonstrates a complete workflow for using templates to send contracts. @@ -996,16 +1206,29 @@ async function sendEmploymentContract(employeeData: { subject: `Employment Contract for ${employeeData.name}`, message: `Hi ${employeeData.name},\n\nPlease review and sign your employment contract.`, }, - distributeDocument: true, + distributeDocument: false, externalId: `emp-contract-${Date.now()}`, }), }); const document = await documentResponse.json(); + // 5. Distribute the envelope and get recipient signing links + const distributionResponse = await fetch(`${BASE_URL}/envelope/distribute`, { + method: 'POST', + headers: { + Authorization: API_TOKEN, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + envelopeId: document.envelopeId, + }), + }); + const distribution = await distributionResponse.json(); + return { - documentId: document.id, - signingUrl: document.recipients[0].signingUrl, + envelopeId: document.envelopeId, + signingUrl: distribution.recipients[0].signingUrl, }; } @@ -1018,7 +1241,7 @@ const result = await sendEmploymentContract({ startDate: '2025-03-01', }); -console.log('Document created:', result.documentId); +console.log('Document created:', result.envelopeId); console.log('Signing URL:', result.signingUrl); ```` diff --git a/apps/docs/content/docs/developers/examples/common-workflows.mdx b/apps/docs/content/docs/developers/examples/common-workflows.mdx index 5cac47f79..02f99506b 100644 --- a/apps/docs/content/docs/developers/examples/common-workflows.mdx +++ b/apps/docs/content/docs/developers/examples/common-workflows.mdx @@ -262,8 +262,11 @@ Use templates for repeatable document workflows. This example creates an employm Map template fields by label and build a prefillFields array - Call POST /template/use with recipients, prefill data, and{' '} - distributeDocument: true + Call POST /template/use with recipients and prefill data + + + Distribute the returned envelope via POST /envelope/distribute and read its signing + links @@ -297,7 +300,7 @@ type TemplateRecipient = { async function sendEmploymentContract( templateId: number, employee: EmployeeData, -): Promise<{ documentId: string; signingUrl: string }> { +): Promise<{ documentId: number; signingUrl: string }> { const templateResponse = await fetch(`${BASE_URL}/template/${templateId}`, { headers: { Authorization: API_TOKEN }, }); @@ -374,7 +377,6 @@ async function sendEmploymentContract( subject: `Your Employment Contract at ${employee.department}`, message: `Hi ${employee.name},\n\nPlease review and sign your employment contract for the ${employee.position} position.\n\nStart date: ${employee.startDate}`, }, - distributeDocument: true, externalId: `emp-${Date.now()}-${employee.email.split('@')[0]}`, }), }); @@ -386,9 +388,25 @@ async function sendEmploymentContract( const document = await createResponse.json(); + const distributeResponse = await fetch(`${BASE_URL}/envelope/distribute`, { + method: 'POST', + headers: { + Authorization: API_TOKEN, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ envelopeId: document.envelopeId }), + }); + + if (!distributeResponse.ok) { + const error = await distributeResponse.json(); + throw new Error(`Failed to send document: ${error.message}`); + } + + const distributeResult = await distributeResponse.json(); + return { documentId: document.id, - signingUrl: document.recipients[0].signingUrl, + signingUrl: distributeResult.recipients[0].signingUrl, }; } @@ -453,12 +471,17 @@ RESPONSE=$(curl -s -X POST "${BASE_URL}/template/use" \ \"subject\": \"Your Employment Contract\", \"message\": \"Please review and sign your employment contract.\" }, - \"distributeDocument\": true, \"externalId\": \"emp-$(date +%s)-alice\" }") -echo "Document created:" -echo $RESPONSE | jq '{id, signingUrl: .recipients[0].signingUrl}' +ENVELOPE_ID=$(echo $RESPONSE | jq -r '.envelopeId') +DISTRIBUTE_RESPONSE=$(curl -s -X POST "${BASE_URL}/envelope/distribute" \ + -H "Authorization: ${API_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"envelopeId\": \"${ENVELOPE_ID}\"}") + +echo "Document created: $(echo $RESPONSE | jq -r '.id')" +echo "Signing URL: $(echo $DISTRIBUTE_RESPONSE | jq -r '.recipients[0].signingUrl')" ```` @@ -476,8 +499,10 @@ Send the same document to multiple recipients in parallel. Useful for policy ack Fetch the template and get the signer recipient slot ID - For each recipient, call POST /template/use with{' '} - distributeDocument: true + For each recipient, call POST /template/use + + + Distribute each returned envelope via POST /envelope/distribute Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute) @@ -540,7 +565,6 @@ async function bulkSendFromTemplate( recipients: [ { id: signerSlot.id, email: recipient.email, name: recipient.name }, ], - distributeDocument: true, externalId: `bulk-${Date.now()}-${recipient.email}`, }), }); @@ -551,10 +575,26 @@ async function bulkSendFromTemplate( } const document = await response.json(); + + const distributeResponse = await fetch(`${BASE_URL}/envelope/distribute`, { + method: 'POST', + headers: { + Authorization: API_TOKEN, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ envelopeId: document.envelopeId }), + }); + + if (!distributeResponse.ok) { + const error = await distributeResponse.json(); + throw new Error(error.message || 'Failed to distribute document'); + } + + const distributeResult = await distributeResponse.json(); return { email: recipient.email, - envelopeId: document.id, - signingUrl: document.recipients[0].signingUrl, + envelopeId: document.envelopeId, + signingUrl: distributeResult.recipients[0].signingUrl, }; }), ); @@ -627,14 +667,24 @@ for RECIPIENT in "${RECIPIENTS[@]}"; do \"email\": \"${EMAIL}\", \"name\": \"${NAME}\" }], - \"distributeDocument\": true, \"externalId\": \"bulk-$(date +%s)-${EMAIL}\" }") - if echo $RESPONSE | jq -e '.id' > /dev/null 2>&1; then - echo " Success: $(echo $RESPONSE | jq -r '.id')" + if echo $RESPONSE | jq -e '.envelopeId' > /dev/null 2>&1; then + ENVELOPE_ID=$(echo $RESPONSE | jq -r '.envelopeId') + DISTRIBUTE_RESPONSE=$(curl -s -X POST "${BASE_URL}/envelope/distribute" \ + -H "Authorization: ${API_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"envelopeId\": \"${ENVELOPE_ID}\"}") + + if echo $DISTRIBUTE_RESPONSE | jq -e '.success' > /dev/null 2>&1; then + echo " Success: ${ENVELOPE_ID}" + echo " Signing URL: $(echo $DISTRIBUTE_RESPONSE | jq -r '.recipients[0].signingUrl')" + else + echo " Failed to distribute: $(echo $DISTRIBUTE_RESPONSE | jq -r '.message')" + fi else - echo " Failed: $(echo $RESPONSE | jq -r '.message')" + echo " Failed to create: $(echo $RESPONSE | jq -r '.message')" fi # Rate limiting delay @@ -837,13 +887,15 @@ After a document is completed, download the signed PDF with all signatures embed +The `version` query parameter accepts `original`, `pending`, or `signed`. + ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; -type DownloadVersion = 'signed' | 'original'; +type DownloadVersion = 'original' | 'pending' | 'signed'; async function downloadDocument( envelopeId: string, @@ -897,7 +949,7 @@ async function downloadAllCompletedDocuments(outputDir: string): Promise { { headers: { Authorization: API_TOKEN } }, ); - const { data, pagination } = await response.json(); + const { data, count, currentPage, perPage, totalPages } = await response.json(); for (const envelope of data) { try { @@ -911,8 +963,8 @@ async function downloadAllCompletedDocuments(outputDir: string): Promise { await new Promise((resolve) => setTimeout(resolve, 500)); } - hasMore = page < pagination.totalPages; - page++; + hasMore = currentPage < totalPages; + page = currentPage + 1; } } diff --git a/apps/docs/content/docs/developers/getting-started/authentication.mdx b/apps/docs/content/docs/developers/getting-started/authentication.mdx index 82d42bb1a..42d4311da 100644 --- a/apps/docs/content/docs/developers/getting-started/authentication.mdx +++ b/apps/docs/content/docs/developers/getting-started/authentication.mdx @@ -24,20 +24,18 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'; {/* prettier-ignore */} -### Open settings +### Select a team -- Log in to your Documenso account -- Click your avatar in the top right corner -- Select **Settings** from the dropdown menu - -![User dropdown menu](/public-api-images/documenso-user-dropdown-menu.webp) +Log in to Documenso and open the team that the integration should access. API tokens are scoped to a +team. -### Navigate to the API Tokens tab +### Open API Tokens -Go to **Settings** and open the **API Tokens** tab. +Go to **Team Settings** → **API Tokens**, or open +`/t/{teamUrl}/settings/tokens` after replacing `{teamUrl}` with your team's URL. ![API tokens page](/public-api-images/api-tokens-page-documenso.webp) @@ -48,7 +46,7 @@ Go to **Settings** and open the **API Tokens** tab. - Click **Create Token** - Enter a descriptive name (e.g., `production-backend`, `zapier-integration`) -- Select an expiration period: never expires, 7 days, 1 month, 3 months, 6 months, or 1 year +- Select an expiration period: 7 days, 1 month, 3 months, 6 months, 12 months, or Never - Click **Create Token** @@ -75,21 +73,21 @@ Include the token in the `Authorization` header of your HTTP requests. ### cURL ```bash -curl https://app.documenso.com/api/v2/document \ +curl https://app.documenso.com/api/v2/envelope \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ### JavaScript / TypeScript ```typescript -const response = await fetch('https://app.documenso.com/api/v2/document', { +const response = await fetch('https://app.documenso.com/api/v2/envelope', { method: 'GET', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, }); -const documents = await response.json(); +const envelopes = await response.json(); ``` ### Using the TypeScript SDK @@ -129,7 +127,7 @@ SDKs are available for [TypeScript](https://github.com/documenso/sdk-typescript) ## Token Security -API tokens grant full access to your account. Follow these practices to keep them secure: +API tokens grant full API access to the team they were created for. Follow these practices to keep them secure: - **Never commit tokens to version control.** Use environment variables instead. - **Use descriptive names.** Names like `zapier-prod` or `backend-staging` help you identify token usage. @@ -155,12 +153,11 @@ const client = new Documenso({ ## Token Scope -API tokens have full access to your account, including: +API tokens have full API access to the team they were created for, including: - Creating, reading, updating, and deleting documents - Managing recipients and fields - Accessing templates -- Managing team resources (if the token owner has team access) There is currently no way to create tokens with limited scopes or permissions. @@ -171,7 +168,7 @@ To revoke a token: {/* prettier-ignore */} - Go to **Settings** > **API Tokens** + Go to **Team Settings** → **API Tokens** Find the token you want to revoke @@ -194,7 +191,7 @@ Revoked tokens stop working immediately. Any integrations using that token will Create a new token in settings. - Ensure you're accessing resources owned by the token's account. + Ensure you're accessing resources owned by the token's team. diff --git a/apps/docs/content/docs/developers/getting-started/first-api-call.mdx b/apps/docs/content/docs/developers/getting-started/first-api-call.mdx index e87b85438..3f9083959 100644 --- a/apps/docs/content/docs/developers/getting-started/first-api-call.mdx +++ b/apps/docs/content/docs/developers/getting-started/first-api-call.mdx @@ -78,12 +78,10 @@ A successful response returns a list of your documents (envelopes): "createdAt": "2025-01-15T10:30:00.000Z" } ], - "pagination": { - "page": 1, - "perPage": 10, - "totalPages": 1, - "totalItems": 1 - } + "count": 1, + "currentPage": 1, + "perPage": 10, + "totalPages": 1 } ```` @@ -228,9 +226,12 @@ After creating a document, it's in `DRAFT` status. To send it to recipients, use ```bash -curl -X POST "https://app.documenso.com/api/v2/envelope/envelope_abc123/distribute" \ +curl -X POST "https://app.documenso.com/api/v2/envelope/distribute" \ -H "Authorization: YOUR_API_TOKEN" \ - -H "Content-Type: application/json" + -H "Content-Type: application/json" \ + -d '{ + "envelopeId": "envelope_abc123" + }' ```` @@ -238,16 +239,14 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/envelope_abc123/distribu ```javascript const envelopeId = 'envelope_abc123'; -const response = await fetch( - `https://app.documenso.com/api/v2/envelope/${envelopeId}/distribute`, - { - method: 'POST', - headers: { - Authorization: 'YOUR_API_TOKEN', - 'Content-Type': 'application/json', - }, +const response = await fetch('https://app.documenso.com/api/v2/envelope/distribute', { + method: 'POST', + headers: { + Authorization: 'YOUR_API_TOKEN', + 'Content-Type': 'application/json', }, -); + body: JSON.stringify({ envelopeId }), +}); const data = await response.json(); console.log('Document sent:', data); @@ -337,16 +336,14 @@ async function createAndSendDocument(pdfPath, recipientEmail, recipientName) { console.log('Created envelope:', envelope.id); // Step 2: Send the document for signing - const distributeResponse = await fetch( - `${BASE_URL}/envelope/${envelope.id}/distribute`, - { - method: 'POST', - headers: { - 'Authorization': API_TOKEN, - 'Content-Type': 'application/json', - }, - } - ); + const distributeResponse = await fetch(`${BASE_URL}/envelope/distribute`, { + method: 'POST', + headers: { + 'Authorization': API_TOKEN, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ envelopeId: envelope.id }), + }); if (!distributeResponse.ok) { const error = await distributeResponse.json(); @@ -422,9 +419,12 @@ echo "Created envelope: ${ENVELOPE_ID}" # Step 2: Send the document for signing echo "Sending document..." -curl -s -X POST "${BASE_URL}/envelope/${ENVELOPE_ID}/distribute" \ +curl -s -X POST "${BASE_URL}/envelope/distribute" \ -H "Authorization: ${API_TOKEN}" \ - -H "Content-Type: application/json" + -H "Content-Type: application/json" \ + -d "{ + \"envelopeId\": \"${ENVELOPE_ID}\" + }" echo "Document sent for signing!" @@ -441,7 +441,7 @@ The API returns standard HTTP status codes and JSON error responses: | `400` | Bad request - check your request payload | | `401` | Unauthorized - invalid or missing API token | | `404` | Not found - resource doesn't exist | -| `429` | Rate limited - wait 60 seconds and retry | +| `429` | Rate limit or plan quota. If `Retry-After` is present, retry the request. | | `500` | Server error - retry or contact support | ### Error Response Format @@ -485,7 +485,9 @@ The API returns standard HTTP status codes and JSON error responses: ### Handling Rate Limits -The API allows 1000 requests per minute per IP address. Your organisation may have its own lower rate limits. When rate limited, wait at least 60 seconds before retrying: +The API has a limit of 1000 requests per minute for each IP address, and your organisation can have a lower limit. Each response includes `X-RateLimit-Remaining` and `X-RateLimit-Reset` (an epoch timestamp in seconds). A windowed rate-limit `429` response includes `Retry-After`. Wait for that number of seconds before you send the request again. A quota `429` response does not include `Retry-After` because a wait cannot correct the quota error. If `Retry-After` is not present, do not send the request again automatically. + +Refer to [Error Handling Patterns](/docs/developers/examples/common-workflows#error-handling-patterns) for more retry information. ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { @@ -493,8 +495,15 @@ async function fetchWithRetry(url, options, maxRetries = 3) { const response = await fetch(url, options); if (response.status === 429) { - console.log('Rate limited, waiting 60 seconds...'); - await new Promise((resolve) => setTimeout(resolve, 60000)); + const retryAfter = response.headers.get('Retry-After'); + + if (!retryAfter) { + return response; + } + + const retryAfterSeconds = Number.parseInt(retryAfter, 10); + console.log(`Rate limit. Wait ${retryAfterSeconds} seconds...`); + await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000)); continue; } diff --git a/apps/remix/app/components/general/app-command-menu.tsx b/apps/remix/app/components/general/app-command-menu.tsx index 2e42b5c4e..7a53ff249 100644 --- a/apps/remix/app/components/general/app-command-menu.tsx +++ b/apps/remix/app/components/general/app-command-menu.tsx @@ -599,18 +599,10 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => { isVisibleCountCapped ? ( {formatChipCount(totalVisibleCount, isVisibleCountCapped)} results ) : ( - + ) ) : ( - + )} diff --git a/packages/lib/utils/render-custom-email-template.test.ts b/packages/lib/utils/render-custom-email-template.test.ts index b1bd4d2c7..d790f60fd 100644 --- a/packages/lib/utils/render-custom-email-template.test.ts +++ b/packages/lib/utils/render-custom-email-template.test.ts @@ -8,15 +8,15 @@ describe('renderCustomEmailTemplate', () => { }); it('replaces multiple variables separated by whitespace', () => { - expect( - renderCustomEmailTemplate('Hi {name}, sign at {url}', { name: 'Sam', url: 'https://x' }), - ).toBe('Hi Sam, sign at https://x'); + expect(renderCustomEmailTemplate('Hi {name}, sign at {url}', { name: 'Sam', url: 'https://x' })).toBe( + 'Hi Sam, sign at https://x', + ); }); it('replaces adjacent variables and variables separated by a non-whitespace character', () => { - expect( - renderCustomEmailTemplate('{day}/{month}/{year}', { day: '01', month: '02', year: '2026' }), - ).toBe('01/02/2026'); + expect(renderCustomEmailTemplate('{day}/{month}/{year}', { day: '01', month: '02', year: '2026' })).toBe( + '01/02/2026', + ); expect( renderCustomEmailTemplate('{firstName}-{lastName}', { diff --git a/packages/trpc/server/envelope-router/envelope-fields/update-envelope-fields.ts b/packages/trpc/server/envelope-router/envelope-fields/update-envelope-fields.ts index 390caea84..735dd5ab3 100644 --- a/packages/trpc/server/envelope-router/envelope-fields/update-envelope-fields.ts +++ b/packages/trpc/server/envelope-router/envelope-fields/update-envelope-fields.ts @@ -29,7 +29,17 @@ export const updateEnvelopeFieldsRoute = authenticatedProcedure id: envelopeId, }, type: null, - fields, + fields: fields.map((field) => ({ + id: field.id, + type: field.type, + pageNumber: field.page, + pageX: field.positionX, + pageY: field.positionY, + width: field.width, + height: field.height, + fieldMeta: field.fieldMeta, + envelopeItemId: field.envelopeItemId, + })), requestMetadata: ctx.metadata, });