diff --git a/apps/docs/content/docs/developers/api/common-errors.mdx b/apps/docs/content/docs/developers/api/common-errors.mdx index f26eba13c..5b1a3f5f1 100644 --- a/apps/docs/content/docs/developers/api/common-errors.mdx +++ b/apps/docs/content/docs/developers/api/common-errors.mdx @@ -37,7 +37,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/recipients.mdx b/apps/docs/content/docs/developers/api/recipients.mdx index 6751a400a..1f180b3bb 100644 --- a/apps/docs/content/docs/developers/api/recipients.mdx +++ b/apps/docs/content/docs/developers/api/recipients.mdx @@ -16,7 +16,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 +134,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 +164,7 @@ const response = await fetch( 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeId: 'clu1abc2def3ghi4jkl', + envelopeId: 'envelope_abcdefhiklmnorst', data: [ { email: 'signer@example.com', @@ -196,7 +196,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 +209,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 +262,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 +284,7 @@ const response = await fetch( 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeId: 'clu1abc2def3ghi4jkl', + envelopeId: 'envelope_abcdefhiklmnorst', data: [ { id: 789, @@ -387,7 +387,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 +462,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/examples/common-workflows.mdx b/apps/docs/content/docs/developers/examples/common-workflows.mdx index 5bdf32cdf..1d6614e01 100644 --- a/apps/docs/content/docs/developers/examples/common-workflows.mdx +++ b/apps/docs/content/docs/developers/examples/common-workflows.mdx @@ -256,8 +256,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 @@ -291,7 +294,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 }, }); @@ -368,7 +371,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]}`, }), }); @@ -380,9 +382,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, }; } @@ -447,12 +465,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')" ```` @@ -470,8 +493,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) @@ -534,7 +559,6 @@ async function bulkSendFromTemplate( recipients: [ { id: signerSlot.id, email: recipient.email, name: recipient.name }, ], - distributeDocument: true, externalId: `bulk-${Date.now()}-${recipient.email}`, }), }); @@ -545,10 +569,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, }; }), ); @@ -621,14 +661,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 @@ -831,13 +881,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, @@ -891,7 +943,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 { @@ -905,8 +957,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.