mirror of
https://github.com/documenso/documenso.git
synced 2026-08-15 02:53:32 +10:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
</Step>
|
||||
<Step>
|
||||
Call <code>POST /template/use</code> with recipients, prefill data, and{' '}
|
||||
<code>distributeDocument: true</code>
|
||||
Call <code>POST /template/use</code> with recipients and prefill data
|
||||
</Step>
|
||||
<Step>
|
||||
Distribute the returned envelope via <code>POST /envelope/distribute</code> and read its signing
|
||||
links
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -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')"
|
||||
````
|
||||
|
||||
</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
|
||||
</Step>
|
||||
<Step>
|
||||
For each recipient, call <code>POST /template/use</code> with{' '}
|
||||
<code>distributeDocument: true</code>
|
||||
For each recipient, call <code>POST /template/use</code>
|
||||
</Step>
|
||||
<Step>
|
||||
Distribute each returned envelope via <code>POST /envelope/distribute</code>
|
||||
</Step>
|
||||
<Step>
|
||||
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
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The `version` query parameter accepts `original`, `pending`, or `signed`.
|
||||
|
||||
<Tabs items={['TypeScript', 'curl']}>
|
||||
<Tab value="TypeScript">
|
||||
```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<void> {
|
||||
{ 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<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
hasMore = page < pagination.totalPages;
|
||||
page++;
|
||||
hasMore = currentPage < totalPages;
|
||||
page = currentPage + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,20 +24,18 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### 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
|
||||
|
||||

|
||||
Log in to Documenso and open the team that the integration should access. API tokens are scoped to a
|
||||
team.
|
||||
|
||||
</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.
|
||||
|
||||

|
||||
|
||||
@@ -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**
|
||||
|
||||
</Step>
|
||||
@@ -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 */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Go to **Settings** > **API Tokens**
|
||||
Go to **Team Settings** → **API Tokens**
|
||||
</Step>
|
||||
<Step>
|
||||
Find the token you want to revoke
|
||||
@@ -194,7 +191,7 @@ Revoked tokens stop working immediately. Any integrations using that token will
|
||||
</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">
|
||||
Ensure you're accessing resources owned by the token's account.
|
||||
Ensure you're accessing resources owned by the token's team.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user