Overview
Every document in Documenso follows a well-defined lifecycle from creation to completion. Understanding this lifecycle is crucial for building integrations, managing workflows, and troubleshooting issues.
Core Document States
Documenso uses four primary states defined in the DocumentStatus enum:
enum DocumentStatus {
DRAFT
PENDING
COMPLETED
REJECTED
}
State Characteristics
DRAFT
PENDING
COMPLETED
REJECTED
Initial creation stateA document enters this state when first created. It represents a work-in-progress that hasn’t been sent to recipients.Allowed Operations:
- Add/remove recipients
- Add/remove/modify fields
- Update document settings
- Upload new document version
- Delete document
- Send document (transitions to PENDING)
Characteristics:
- No recipient notifications sent
- Full edit capabilities
- Not visible to recipients
- Can be saved as template
- No audit trail started
Database Fields:{
status: 'DRAFT',
createdAt: DateTime,
updatedAt: DateTime,
completedAt: null,
deletedAt: null
}
Active signing stateDocument has been sent to recipients and is awaiting completion.Allowed Operations:
- View document and fields
- Track recipient progress
- Send reminders
- Cancel document
- Recipient can complete fields
- Recipient can reject document
Restricted Operations:
- Cannot edit recipients
- Cannot modify fields
- Cannot change document settings
- Cannot delete (must cancel first)
Characteristics:
- Recipients have access via token
- Email notifications active
- Audit log tracking all events
- May have expiration date
- Webhook events triggered
Database Fields:{
status: 'PENDING',
createdAt: DateTime,
updatedAt: DateTime,
completedAt: null,
deletedAt: null,
recipients: [
{
sendStatus: 'SENT',
signingStatus: 'NOT_SIGNED' | 'SIGNED',
signedAt: DateTime?
}
]
}
Successfully completed stateAll recipients have completed their required actions.Allowed Operations:
- Download signed PDF
- Download certificate
- Download audit log
- View completion details
- Share via link
Restricted Operations:
- Cannot modify anything
- Cannot resend
- Cannot add recipients
- Effectively immutable
Characteristics:
- All recipients have
SIGNED status
- Sealed PDF generated
- Certificate attached
- QR verification token created
- Completion emails sent
- Final webhooks triggered
Database Fields:{
status: 'COMPLETED',
createdAt: DateTime,
updatedAt: DateTime,
completedAt: DateTime,
deletedAt: null,
qrToken: string,
recipients: [
{
sendStatus: 'SENT',
signingStatus: 'SIGNED',
signedAt: DateTime
}
]
}
Rejection stateAt least one recipient has explicitly rejected the document.Allowed Operations:
- View rejection reason
- Download partial audit log
- View which recipients signed before rejection
Restricted Operations:
- Cannot continue signing process
- Cannot modify document
- Cannot resend to recipients
Characteristics:
- Signing process terminated
- Rejection reason recorded
- Remaining recipients blocked
- Partial audit log available
- Rejection webhooks triggered
Database Fields:{
status: 'REJECTED',
createdAt: DateTime,
updatedAt: DateTime,
completedAt: null,
deletedAt: null,
recipients: [
{
signingStatus: 'REJECTED',
rejectionReason: string,
signedAt: null
}
]
}
State Transition Diagram
State Transitions
DRAFT → PENDING
Trigger: sendDocument() function called
Prerequisites:
- At least one recipient exists
- All recipients have valid email addresses
- All recipients have required fields assigned
- All field validations pass
- Document source file exists
Actions Performed:
- Validate document and recipients
- Update document status to
PENDING
- Set recipient
sendStatus to SENT
- Send email notifications (based on signing order)
- Create
DOCUMENT_SENT audit log entry
- Trigger
DOCUMENT_SENT webhook
- Set expiration date if configured
Code Reference:
// packages/lib/server-only/document/send-document.ts
await prisma.envelope.update({
where: { id: envelope.id },
data: { status: DocumentStatus.PENDING }
});
PENDING → COMPLETED
Trigger: Last recipient completes their action
Prerequisites:
- All recipients have
signingStatus: SIGNED
- All required fields completed
- No recipients have rejected
Actions Performed:
- Update document status to
COMPLETED
- Set
completedAt timestamp
- Generate sealed PDF with all field data
- Generate signing certificate
- Append certificate to PDF
- Optionally append audit log
- Generate QR verification token
- Send completion emails to all parties
- Create
DOCUMENT_COMPLETED audit log entry
- Trigger
DOCUMENT_COMPLETED webhook
Code Reference:
// packages/lib/jobs/definitions/internal/seal-document.handler.ts
const isCompleted = isDocumentCompleted(envelope);
if (isCompleted) {
await prisma.envelope.update({
where: { id: envelope.id },
data: {
status: DocumentStatus.COMPLETED,
completedAt: new Date()
}
});
}
PENDING → REJECTED
Trigger: Any recipient explicitly rejects the document
Prerequisites:
- Document is in
PENDING state
- Recipient provides rejection reason (optional)
Actions Performed:
- Update document status to
REJECTED
- Set recipient
signingStatus to REJECTED
- Record rejection reason
- Block remaining recipients from accessing
- Create
DOCUMENT_REJECTED audit log entry
- Trigger
DOCUMENT_REJECTED webhook
- Send rejection notification emails
DRAFT → Deleted
Trigger: Document owner deletes a draft
Prerequisites:
- Document is in
DRAFT state
- User has permission to delete
Actions Performed:
- Set
deletedAt timestamp (soft delete)
- Remove from active document listings
- Optionally hard delete after retention period
Extended Status Views
Documenso uses virtual status filters for organizing documents:
INBOX
Not a database state, but a filter showing documents requiring your action:
const inboxDocuments = documents.filter(doc =>
doc.recipients.some(r =>
r.email === userEmail &&
r.signingStatus === 'NOT_SIGNED' &&
doc.status === 'PENDING'
)
);
ALL
A filter showing all documents regardless of status (except deleted).
Lifecycle Events Timeline
A typical document’s lifecycle events:
1. DOCUMENT_CREATED (DRAFT)
↓ 2 hours later
2. DOCUMENT_SENT (PENDING)
↓ 10 minutes later
3. DOCUMENT_OPENED (recipient 1)
↓ 5 minutes later
4. DOCUMENT_SIGNED (recipient 1)
↓ 2 hours later
5. DOCUMENT_OPENED (recipient 2)
↓ 3 minutes later
6. DOCUMENT_SIGNED (recipient 2)
↓ immediately
7. DOCUMENT_COMPLETED (COMPLETED)
Recipient Status Lifecycle
Recipients have their own status independent of document status:
Send Status
enum SendStatus {
NOT_SENT // Email not yet sent
SENT // Email delivered
}
Read Status
enum ReadStatus {
NOT_OPENED // Recipient hasn't viewed document
OPENED // Recipient has viewed document
}
Signing Status
enum SigningStatus {
NOT_SIGNED // Recipient hasn't completed fields
SIGNED // Recipient completed all fields
REJECTED // Recipient rejected the document
}
Expiration Handling
Documents in PENDING state can have expiration dates:
type EnvelopeExpirationPeriod = {
count: number;
unit: 'days' | 'weeks' | 'months';
};
When a Document Expires:
- Recipients can no longer access the document
- Document status remains
PENDING (not auto-rejected)
- Expiration notification sent
RECIPIENT_EXPIRED webhook triggered
- Sender can extend expiration or cancel document
Best Practices
State Management
- Always check current state before performing operations
- Handle race conditions when multiple recipients act simultaneously
- Use transactions for state transitions affecting multiple records
- Validate prerequisites before each transition
Monitoring Lifecycle
// Good: Check document state before operation
if (document.status !== DocumentStatus.PENDING) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Document is not in a signable state'
});
}
Error Recovery
- Implement idempotency for state transitions
- Log all state changes with metadata
- Use background jobs for asynchronous completion tasks
- Handle partial failures gracefully
Database Schema
Key fields tracking document lifecycle:
model Envelope {
id String @id
status DocumentStatus @default(DRAFT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
deletedAt DateTime?
qrToken String?
recipients Recipient[]
fields Field[]
auditLogs DocumentAuditLog[]
}
model Recipient {
id Int @id
sendStatus SendStatus @default(NOT_SENT)
readStatus ReadStatus @default(NOT_OPENED)
signingStatus SigningStatus @default(NOT_SIGNED)
signedAt DateTime?
expiresAt DateTime?
}
State transitions are irreversible once a document reaches COMPLETED or REJECTED. Design your workflows with this immutability in mind.
API Status Queries
Check Document Status
GET /api/v1/documents/:id
Response:
{
"id": "doc_123",
"status": "PENDING",
"createdAt": "2024-03-15T10:00:00Z",
"completedAt": null,
"recipients": [
{
"email": "[email protected]",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT",
"readStatus": "OPENED"
}
]
}
Monitor State Changes
Use webhooks to track lifecycle events in real-time:
{
"event": "DOCUMENT_SIGNED",
"createdAt": "2024-03-15T10:30:00Z",
"data": {
"documentId": "doc_123",
"recipientEmail": "[email protected]",
"status": "PENDING"
}
}