Skip to main content

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

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
}

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:
  1. Validate document and recipients
  2. Update document status to PENDING
  3. Set recipient sendStatus to SENT
  4. Send email notifications (based on signing order)
  5. Create DOCUMENT_SENT audit log entry
  6. Trigger DOCUMENT_SENT webhook
  7. 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:
  1. Update document status to COMPLETED
  2. Set completedAt timestamp
  3. Generate sealed PDF with all field data
  4. Generate signing certificate
  5. Append certificate to PDF
  6. Optionally append audit log
  7. Generate QR verification token
  8. Send completion emails to all parties
  9. Create DOCUMENT_COMPLETED audit log entry
  10. 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:
  1. Update document status to REJECTED
  2. Set recipient signingStatus to REJECTED
  3. Record rejection reason
  4. Block remaining recipients from accessing
  5. Create DOCUMENT_REJECTED audit log entry
  6. Trigger DOCUMENT_REJECTED webhook
  7. 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:
  1. Set deletedAt timestamp (soft delete)
  2. Remove from active document listings
  3. 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:
  1. Recipients can no longer access the document
  2. Document status remains PENDING (not auto-rejected)
  3. Expiration notification sent
  4. RECIPIENT_EXPIRED webhook triggered
  5. 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"
  }
}

Build docs developers (and LLMs) love