docs: fix workflows, authentication, error codes and id formats

- workflows: flat pagination shape, signing URLs via /envelope/distribute, pending version value
- authentication: tokens are team-scoped; fix expiry options and use GET /envelope for the check
- recipients: envelope IDs use the envelope_ prefix format, not cuid
- common-errors: add ENVELOPE_CANCELLED, ENVELOPE_TSP_LOCKED and CSC_* codes
This commit is contained in:
ephraimduncan
2026-07-30 22:04:01 +00:00
parent f353706e24
commit 4e87121bef
4 changed files with 117 additions and 47 deletions
@@ -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_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_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_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_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 ## See Also
@@ -16,7 +16,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
```json ```json
{ {
"id": 123, "id": 123,
"envelopeId": "clu1abc2def3ghi4jkl", "envelopeId": "envelope_abcdefhiklmnorst",
"email": "signer@example.com", "email": "signer@example.com",
"name": "John Doe", "name": "John Doe",
"role": "SIGNER", "role": "SIGNER",
@@ -134,7 +134,7 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/create-many" \
-H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"envelopeId": "clu1abc2def3ghi4jkl", "envelopeId": "envelope_abcdefhiklmnorst",
"data": [ "data": [
{ {
"email": "signer@example.com", "email": "signer@example.com",
@@ -164,7 +164,7 @@ const response = await fetch(
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
envelopeId: 'clu1abc2def3ghi4jkl', envelopeId: 'envelope_abcdefhiklmnorst',
data: [ data: [
{ {
email: 'signer@example.com', email: 'signer@example.com',
@@ -196,7 +196,7 @@ const { data: recipients } = await response.json();
"data": [ "data": [
{ {
"id": 789, "id": 789,
"envelopeId": "clu1abc2def3ghi4jkl", "envelopeId": "envelope_abcdefhiklmnorst",
"email": "signer@example.com", "email": "signer@example.com",
"name": "John Doe", "name": "John Doe",
"role": "SIGNER", "role": "SIGNER",
@@ -209,7 +209,7 @@ const { data: recipients } = await response.json();
}, },
{ {
"id": 790, "id": 790,
"envelopeId": "clu1abc2def3ghi4jkl", "envelopeId": "envelope_abcdefhiklmnorst",
"email": "approver@example.com", "email": "approver@example.com",
"name": "Jane Smith", "name": "Jane Smith",
"role": "APPROVER", "role": "APPROVER",
@@ -262,7 +262,7 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/update-many" \
-H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"envelopeId": "clu1abc2def3ghi4jkl", "envelopeId": "envelope_abcdefhiklmnorst",
"data": [ "data": [
{ {
"id": 789, "id": 789,
@@ -284,7 +284,7 @@ const response = await fetch(
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
envelopeId: 'clu1abc2def3ghi4jkl', envelopeId: 'envelope_abcdefhiklmnorst',
data: [ data: [
{ {
id: 789, id: 789,
@@ -387,7 +387,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/recipien
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
envelopeId: 'clu1abc2def3ghi4jkl', envelopeId: 'envelope_abcdefhiklmnorst',
data: [ data: [
{ {
email: 'approver@example.com', email: 'approver@example.com',
@@ -462,7 +462,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/recipien
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
envelopeId: 'clu1abc2def3ghi4jkl', envelopeId: 'envelope_abcdefhiklmnorst',
data: [ data: [
{ {
email: 'signer@example.com', email: 'signer@example.com',
@@ -256,8 +256,11 @@ Use templates for repeatable document workflows. This example creates an employm
Map template fields by label and build a <code>prefillFields</code> array Map template fields by label and build a <code>prefillFields</code> array
</Step> </Step>
<Step> <Step>
Call <code>POST /template/use</code> with recipients, prefill data, and{' '} Call <code>POST /template/use</code> with recipients and prefill data
<code>distributeDocument: true</code> </Step>
<Step>
Distribute the returned envelope via <code>POST /envelope/distribute</code> and read its signing
links
</Step> </Step>
</Steps> </Steps>
@@ -291,7 +294,7 @@ type TemplateRecipient = {
async function sendEmploymentContract( async function sendEmploymentContract(
templateId: number, templateId: number,
employee: EmployeeData, employee: EmployeeData,
): Promise<{ documentId: string; signingUrl: string }> { ): Promise<{ documentId: number; signingUrl: string }> {
const templateResponse = await fetch(`${BASE_URL}/template/${templateId}`, { const templateResponse = await fetch(`${BASE_URL}/template/${templateId}`, {
headers: { Authorization: API_TOKEN }, headers: { Authorization: API_TOKEN },
}); });
@@ -368,7 +371,6 @@ async function sendEmploymentContract(
subject: `Your Employment Contract at ${employee.department}`, 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}`, 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]}`, externalId: `emp-${Date.now()}-${employee.email.split('@')[0]}`,
}), }),
}); });
@@ -380,9 +382,25 @@ async function sendEmploymentContract(
const document = await createResponse.json(); 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 { return {
documentId: document.id, 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\", \"subject\": \"Your Employment Contract\",
\"message\": \"Please review and sign your employment contract.\" \"message\": \"Please review and sign your employment contract.\"
}, },
\"distributeDocument\": true,
\"externalId\": \"emp-$(date +%s)-alice\" \"externalId\": \"emp-$(date +%s)-alice\"
}") }")
echo "Document created:" ENVELOPE_ID=$(echo $RESPONSE | jq -r '.envelopeId')
echo $RESPONSE | jq '{id, signingUrl: .recipients[0].signingUrl}' 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')"
```` ````
</Tab> </Tab>
@@ -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 Fetch the template and get the signer recipient slot ID
</Step> </Step>
<Step> <Step>
For each recipient, call <code>POST /template/use</code> with{' '} For each recipient, call <code>POST /template/use</code>
<code>distributeDocument: true</code> </Step>
<Step>
Distribute each returned envelope via <code>POST /envelope/distribute</code>
</Step> </Step>
<Step> <Step>
Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute) Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute)
@@ -534,7 +559,6 @@ async function bulkSendFromTemplate(
recipients: [ recipients: [
{ id: signerSlot.id, email: recipient.email, name: recipient.name }, { id: signerSlot.id, email: recipient.email, name: recipient.name },
], ],
distributeDocument: true,
externalId: `bulk-${Date.now()}-${recipient.email}`, externalId: `bulk-${Date.now()}-${recipient.email}`,
}), }),
}); });
@@ -545,10 +569,26 @@ async function bulkSendFromTemplate(
} }
const document = await response.json(); 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 { return {
email: recipient.email, email: recipient.email,
envelopeId: document.id, envelopeId: document.envelopeId,
signingUrl: document.recipients[0].signingUrl, signingUrl: distributeResult.recipients[0].signingUrl,
}; };
}), }),
); );
@@ -621,14 +661,24 @@ for RECIPIENT in "${RECIPIENTS[@]}"; do
\"email\": \"${EMAIL}\", \"email\": \"${EMAIL}\",
\"name\": \"${NAME}\" \"name\": \"${NAME}\"
}], }],
\"distributeDocument\": true,
\"externalId\": \"bulk-$(date +%s)-${EMAIL}\" \"externalId\": \"bulk-$(date +%s)-${EMAIL}\"
}") }")
if echo $RESPONSE | jq -e '.id' > /dev/null 2>&1; then if echo $RESPONSE | jq -e '.envelopeId' > /dev/null 2>&1; then
echo " Success: $(echo $RESPONSE | jq -r '.id')" 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 else
echo " Failed: $(echo $RESPONSE | jq -r '.message')" echo " Failed to create: $(echo $RESPONSE | jq -r '.message')"
fi fi
# Rate limiting delay # Rate limiting delay
@@ -831,13 +881,15 @@ After a document is completed, download the signed PDF with all signatures embed
</Step> </Step>
</Steps> </Steps>
The `version` query parameter accepts `original`, `pending`, or `signed`.
<Tabs items={['TypeScript', 'curl']}> <Tabs items={['TypeScript', 'curl']}>
<Tab value="TypeScript"> <Tab value="TypeScript">
```typescript ```typescript
const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const API_TOKEN = process.env.DOCUMENSO_API_TOKEN;
const BASE_URL = 'https://app.documenso.com/api/v2'; const BASE_URL = 'https://app.documenso.com/api/v2';
type DownloadVersion = 'signed' | 'original'; type DownloadVersion = 'original' | 'pending' | 'signed';
async function downloadDocument( async function downloadDocument(
envelopeId: string, envelopeId: string,
@@ -891,7 +943,7 @@ async function downloadAllCompletedDocuments(outputDir: string): Promise<void> {
{ headers: { Authorization: API_TOKEN } }, { headers: { Authorization: API_TOKEN } },
); );
const { data, pagination } = await response.json(); const { data, count, currentPage, perPage, totalPages } = await response.json();
for (const envelope of data) { for (const envelope of data) {
try { try {
@@ -905,8 +957,8 @@ async function downloadAllCompletedDocuments(outputDir: string): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
} }
hasMore = page < pagination.totalPages; hasMore = currentPage < totalPages;
page++; page = currentPage + 1;
} }
} }
@@ -24,20 +24,18 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
{/* prettier-ignore */} {/* prettier-ignore */}
<Steps> <Steps>
<Step> <Step>
### Open settings ### Select a team
- Log in to your Documenso account Log in to Documenso and open the team that the integration should access. API tokens are scoped to a
- Click your avatar in the top right corner team.
- Select **Settings** from the dropdown menu
![User dropdown menu](/public-api-images/documenso-user-dropdown-menu.webp)
</Step> </Step>
<Step> <Step>
### 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) ![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** - Click **Create Token**
- Enter a descriptive name (e.g., `production-backend`, `zapier-integration`) - 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** - Click **Create Token**
</Step> </Step>
@@ -75,21 +73,21 @@ Include the token in the `Authorization` header of your HTTP requests.
### cURL ### cURL
```bash ```bash
curl https://app.documenso.com/api/v2/document \ curl https://app.documenso.com/api/v2/envelope \
-H "Authorization: api_xxxxxxxxxxxxxxxx" -H "Authorization: api_xxxxxxxxxxxxxxxx"
``` ```
### JavaScript / TypeScript ### JavaScript / TypeScript
```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', method: 'GET',
headers: { headers: {
Authorization: 'api_xxxxxxxxxxxxxxxx', Authorization: 'api_xxxxxxxxxxxxxxxx',
}, },
}); });
const documents = await response.json(); const envelopes = await response.json();
``` ```
### Using the TypeScript SDK ### Using the TypeScript SDK
@@ -129,7 +127,7 @@ SDKs are available for [TypeScript](https://github.com/documenso/sdk-typescript)
## Token Security ## 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. - **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. - **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 ## 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 - Creating, reading, updating, and deleting documents
- Managing recipients and fields - Managing recipients and fields
- Accessing templates - 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. There is currently no way to create tokens with limited scopes or permissions.
@@ -171,7 +168,7 @@ To revoke a token:
{/* prettier-ignore */} {/* prettier-ignore */}
<Steps> <Steps>
<Step> <Step>
Go to **Settings** > **API Tokens** Go to **Team Settings** **API Tokens**
</Step> </Step>
<Step> <Step>
Find the token you want to revoke Find the token you want to revoke
@@ -194,7 +191,7 @@ Revoked tokens stop working immediately. Any integrations using that token will
</Accordion> </Accordion>
<Accordion title="401 Unauthorized — Expired token">Create a new token in settings.</Accordion> <Accordion title="401 Unauthorized — Expired token">Create a new token in settings.</Accordion>
<Accordion title="403 Forbidden — Token doesn't have access to the resource"> <Accordion title="403 Forbidden — Token doesn't have access to the resource">
Ensure you're accessing resources owned by the token's account. Ensure you're accessing resources owned by the token's team.
</Accordion> </Accordion>
</Accordions> </Accordions>