# Documenso Documentation

The open-source document signing platform. Send documents for signatures, integrate with your apps, or self-host with full control.

User Guide

Send documents, create templates, and manage your team using the web application.

Get started →

Developer Guide

Integrate document signing into your applications with the REST API, webhooks, and embedding.

View API docs →

Self-Hosting Guide

Deploy your own Documenso instance with Docker, Kubernetes, or Railway.

Deploy now →

Quick Start

Send your first document

  1. Create an account
  2. Upload and send a document

Integrate with the API

  1. Get your API key
  2. Make your first API call

Deploy self-hosted

  1. Check requirements
  2. Run with Docker

Core Concepts

Document Lifecycle
Draft to completed
Recipient Roles
Signers and approvers
Field Types
Signatures and inputs
Signing Certificates
Digital verification

Compliance & Legal

ESIGN, UETA, eIDAS compliance, GDPR, and signature levels explained.

Policies & Licensing

AGPL and Enterprise licenses, fair use, privacy policy, and support.

Join the Community

Documenso is open source. Contribute, ask questions, or share feedback.

GitHub Discord Try Documenso
# Document Lifecycle ## Document States [#document-states] A document can be in one of four states: | State | Description | | ------------- | ----------------------------------------------------------------- | | **Draft** | Document is being prepared and has not been sent | | **Pending** | Document has been sent and is awaiting recipient actions | | **Completed** | All recipients have completed their required actions | | **Rejected** | A recipient has rejected the document (when rejection is enabled) | ## How a Document Moves Through States [#how-a-document-moves-through-states] ## Draft [#draft] When you upload a document or create one from a template, it starts in the **Draft** state. In this state, you can: * Add and remove recipients * Assign roles to recipients (signer, approver, viewer, CC) * Add, move, and configure fields * Set signing order * Configure document settings (expiration, reminders, rejection) * Delete the document A draft document is only visible to you (the owner) and team members with appropriate permissions. Recipients cannot see or access the document until you send it. **Transition:** A draft becomes **Pending** when you send it to recipients. ## Pending [#pending] Once sent, a document enters the **Pending** state. Recipients receive email notifications with links to view and complete their assigned actions. While pending, you can: * View recipient progress * Resend notifications to recipients * Void the document (cancels all pending actions) You cannot modify the document content, recipients, or fields while it is pending. **Transitions:** * Becomes **Completed** when all recipients finish their required actions * Becomes **Rejected** if any recipient rejects the document (requires rejection to be enabled) ## Completed [#completed] A document reaches the **Completed** state when all recipients have fulfilled their roles: * Signers have signed * Approvers have approved * Viewers have viewed (if view confirmation is required) At completion: * All parties receive a copy of the signed document * The document is sealed with a digital certificate * An audit log is attached showing all actions taken Completed documents cannot be modified. You can download the signed PDF or view the audit trail. ## Rejected [#rejected] If you enable document rejection in settings, recipients can reject instead of signing. When any recipient rejects: * The document immediately moves to **Rejected** state * Other pending recipients can no longer act on the document * The document owner is notified Rejected documents cannot be modified or reactivated. To proceed, you need to create a new document. ## Related Concepts [#related-concepts] * [Recipient Roles](/docs/concepts/recipient-roles) - The different roles recipients can have * [Field Types](/docs/concepts/field-types) - Fields you can add to documents * [Signing Workflow](/docs/concepts/signing-workflow) - How the signing process works for recipients # Field Types ## Field Types Overview [#field-types-overview] | Field Type | Description | Auto-filled | | ---------- | ------------------------------------------------- | ----------- | | Signature | Recipient's signature (drawn, typed, or uploaded) | No | | Initials | Recipient's initials | No | | Email | Recipient's email address | Yes | | Name | Recipient's full name | Yes | | Date | Date the field was completed | Yes | | Text | Free-form text input | No | | Number | Numeric input with optional validation | No | | Radio | Single selection from a list of options | No | | Checkbox | Multiple selections from a list of options | No | | Dropdown | Single selection from a dropdown menu | No | ## Signature [#signature] Signature field in the document editor The signature field captures the recipient's legally binding signature. Recipients can: * **Draw** their signature using a mouse or touchscreen * **Type** their name and select a font style * **Upload** an image of their signature ### Configuration Options [#configuration-options] | Option | Description | | --------- | -------------------------------------------------- | | Required | Whether the field must be completed before signing | | Read-only | Lock the field with a pre-filled value | | Label | Display text shown above the field | ### Common Use Cases [#common-use-cases] * Contract execution * Agreement acceptance * Authorization approvals Each signer must have at least one Signature field assigned to them. ## Initials [#initials] The initials field captures abbreviated signatures, typically used to acknowledge individual pages or clauses. ### Configuration Options [#configuration-options-1] | Option | Description | | -------------- | -------------------------------------- | | Required | Whether the field must be completed | | Read-only | Lock the field with a pre-filled value | | Label | Display text shown above the field | | Text alignment | Left, center, or right alignment | ### Common Use Cases [#common-use-cases-1] * Page acknowledgment * Clause acceptance * Change or amendment approval ## Email [#email] The email field displays the recipient's email address. This field is automatically populated with the email address used to send the signing request. ### Configuration Options [#configuration-options-2] | Option | Description | | -------------- | --------------------------------------------------- | | Required | Whether the field must be completed | | Read-only | Lock the field (recommended for auto-filled values) | | Label | Display text shown above the field | | Text alignment | Left, center, or right alignment | ### Common Use Cases [#common-use-cases-2] * Contact information sections * Identity verification * Record keeping ## Name [#name] The name field captures the recipient's full name. When the recipient has a name on file, the field can be auto-populated. ### Configuration Options [#configuration-options-3] | Option | Description | | -------------- | -------------------------------------- | | Required | Whether the field must be completed | | Read-only | Lock the field with a pre-filled value | | Label | Display text shown above the field | | Text alignment | Left, center, or right alignment | ### Common Use Cases [#common-use-cases-3] * Signature blocks * Party identification * Contact details ## Date [#date] The date field records when the recipient completed the field or signed the document. By default, it auto-fills with the current date. ### Configuration Options [#configuration-options-4] | Option | Description | | -------------- | -------------------------------------- | | Required | Whether the field must be completed | | Read-only | Lock the field with a pre-filled value | | Label | Display text shown above the field | | Text alignment | Left, center, or right alignment | ### Common Use Cases [#common-use-cases-4] * Signature date * Agreement effective date * Timestamp records ## Text [#text] Text field in the document editor The text field accepts free-form text input from recipients. Use this for any information that doesn't fit other field types. ### Configuration Options [#configuration-options-5] | Option | Description | | --------------- | ------------------------------------------ | | Required | Whether the field must be completed | | Read-only | Lock the field with a pre-filled value | | Label | Display text shown above the field | | Placeholder | Hint text shown when the field is empty | | Default value | Pre-filled text that recipients can modify | | Character limit | Maximum number of characters allowed | | Text alignment | Left, center, or right alignment | | Line height | Spacing between lines of text | | Letter spacing | Spacing between characters | ### Rules [#rules] * A field cannot be both required and read-only at the same time * A read-only field must have a default text value (it cannot be empty) * The field is inserted automatically into the document if there is a default text value * The text field character count cannot exceed the character limit * The signer cannot modify a read-only field ### Common Use Cases [#common-use-cases-5] * Address input * Company names * Job titles * Custom information ## Number [#number] The number field accepts numeric input with optional validation constraints. ### Configuration Options [#configuration-options-6] | Option | Description | | -------------- | -------------------------------------------- | | Required | Whether the field must be completed | | Read-only | Lock the field with a pre-filled value | | Label | Display text shown above the field | | Placeholder | Hint text shown when the field is empty | | Default value | Pre-filled number that recipients can modify | | Minimum value | Lowest allowed number | | Maximum value | Highest allowed number | | Number format | Display format for the number | | Text alignment | Left, center, or right alignment | ### Rules [#rules-1] * The value must be a number * A field cannot be both required and read-only at the same time * A read-only field must have a default number value * If a default number and a maximum value are set, the default must be less than the maximum * If a default number and a minimum value are set, the default must be greater than the minimum * The value must match the number format if a number format is set ### Common Use Cases [#common-use-cases-6] * Quantities * Pricing * Phone numbers * Employee IDs ## Radio [#radio] The radio field presents a list of options where the recipient can select exactly one. ### Configuration Options [#configuration-options-7] | Option | Description | | ----------------- | ---------------------------------------- | | Required | Whether a selection must be made | | Read-only | Lock the field with a pre-selected value | | Label | Display text shown above the field | | Options | List of selectable values | | Default selection | Pre-selected option | | Direction | Vertical or horizontal layout | ### Rules [#rules-2] * A field cannot be both required and read-only at the same time * A read-only field must have at least one option * The field auto-signs if there is a default value * The signer cannot select a value that's not in the options list * Only one option can be selected at a time ### Common Use Cases [#common-use-cases-7] * Yes/No questions * Single-choice selections * Status indicators * Plan or tier selection ## Checkbox [#checkbox] Checkbox field in the document editor The checkbox field presents a list of options where the recipient can select multiple items. ### Configuration Options [#configuration-options-8] | Option | Description | | ------------------ | ------------------------------------------- | | Required | Whether at least one selection must be made | | Read-only | Lock the field with pre-selected values | | Label | Display text shown above the field | | Options | List of selectable values | | Default selections | Pre-selected options | | Validation rule | Rules for minimum/maximum selections | | Direction | Vertical or horizontal layout | ### Rules [#rules-3] * A field cannot be both required and read-only at the same time * A read-only field must have at least one checked option * The field auto-signs if there are default values * The validation rule enforces selection counts: "At least", "At most", or "Exactly" a specified number of options * The signer cannot select a value that's not in the options list ### Common Use Cases [#common-use-cases-8] * Terms and conditions acceptance * Multiple acknowledgments * Feature selection * Preference lists ## Dropdown [#dropdown] Dropdown field in the document editor The dropdown field presents a list of options in a collapsible menu. Recipients select one option from the list. ### Configuration Options [#configuration-options-9] | Option | Description | | ------------- | ---------------------------------------- | | Required | Whether a selection must be made | | Read-only | Lock the field with a pre-selected value | | Label | Display text shown above the field | | Options | List of selectable values | | Default value | Pre-selected option | ### Rules [#rules-4] * A field cannot be both required and read-only at the same time * A read-only field must have a default value * The default value must be one of the options * The field auto-signs if there is a default value * The signer cannot select a value that's not in the options list ### Common Use Cases [#common-use-cases-9] * Country or state selection * Department selection * Category classification * Status selection ## Common Configuration Options [#common-configuration-options] All field types share these base configuration options: | Option | Description | Default | | --------- | --------------------------------------------------- | ------- | | Required | Recipient must complete the field to finish signing | `false` | | Read-only | Field value cannot be changed by the recipient | `false` | | Label | Text displayed above or near the field | None | | Font size | Size of the text in the field (8-96px) | 12px | ## Validation [#validation] Fields validate input based on their type and configuration: * **Required fields** must be completed before the recipient can finish signing * **Read-only fields** display pre-filled values that cannot be modified * **Number fields** validate against minimum and maximum values when configured * **Checkbox fields** can enforce a minimum or maximum number of selections If validation fails, the recipient sees an error message and must correct the input before proceeding. ## Related [#related] * [Add Fields to Documents](/docs/users/documents/add-fields) - Learn how to place fields on your documents * [Recipient Roles](/docs/concepts/recipient-roles) - Understand who can be assigned fields * [Fields API](/docs/developers/api/fields) - Programmatically add fields via the API # Concepts ## Core Concepts [#core-concepts] *** ## How These Concepts Apply [#how-these-concepts-apply] These concepts work consistently across all ways you interact with Documenso: * **Web application**: When you create documents in the UI, you'll select recipient roles, add fields, and track documents through their lifecycle states. * **API integration**: The same concepts map directly to API endpoints. Documents have status fields, recipients have role properties, and fields have type configurations. * **Self-hosting**: The signing certificate concept becomes particularly relevant when you deploy your own instance and configure your own certificates for document signing. Understanding these fundamentals will make the rest of the documentation easier to follow. *** ## Related Sections [#related-sections] # Recipient Roles ## Role Overview [#role-overview] | Role | Action Required | Can Sign | Description | | --------- | --------------- | -------- | ---------------------------------------- | | Signer | Yes | Yes | Must sign the document | | Approver | Yes | Optional | Must approve the document | | Viewer | Yes | No | Must view the document | | Assistant | Yes | No | Can pre-fill fields for other recipients | | CC | No | No | Receives a copy after completion | ## Role Details [#role-details] Signers are the primary recipients of a document. They must complete all signature fields assigned to them before the document can be finalized. **What they can do:** * Sign signature fields assigned to them * Fill out any other fields assigned to them (text, date, checkbox, etc.) * Download the document after signing **What they cannot do:** * Sign on behalf of other recipients * Modify fields assigned to other recipients **When to use this role:** * Contracts requiring a legally binding signature * Agreements where the recipient must formally consent * Any document that requires a signature to be valid Approvers must review and approve the document, but signing is optional. The document cannot be completed until all approvers have given their approval. **What they can do:** * Approve or reject the document * Optionally add a signature if signature fields are assigned * Fill out fields assigned to them * Download the document after approval **What they cannot do:** * Complete the document without explicitly approving it * Modify fields assigned to other recipients **When to use this role:** * Documents requiring manager or supervisor approval * Workflows where review is required before final signatures * Compliance processes requiring sign-off from multiple parties Viewers must acknowledge that they have viewed the document. They cannot add signatures but must confirm they have reviewed the content. **What they can do:** * View the complete document * Confirm they have viewed it * Download the document after viewing **What they cannot do:** * Sign the document * Fill out fields (no fields can be assigned to viewers) * Modify the document in any way **When to use this role:** * Informational documents that require acknowledgment * Policies or disclosures that recipients must review * Documents where you need proof of receipt without a signature Assistants can prepare the document by pre-filling fields on behalf of other signers. This role is only available when sequential signing is enabled. **What they can do:** * Pre-fill suggested values in fields assigned to later signers * Help prepare the document for the actual signers * Fill out any fields specifically assigned to them **What they cannot do:** * Sign on behalf of other recipients * Submit the document as complete * Be used in parallel signing mode **When to use this role:** * Administrative staff preparing documents for executives to sign * Workflows where one person gathers information and another signs * Situations where you want to reduce the burden on the final signer The Assistant role requires sequential signing to be enabled. You cannot use this role when recipients sign in parallel. CC recipients receive a copy of the completed document but do not need to take any action. They are notified when the document is fully signed. **What they can do:** * Receive a copy of the completed document * Download the signed document **What they cannot do:** * Sign or approve the document * View the document before it is completed * Take any action that affects document completion **When to use this role:** * Keeping stakeholders informed about signed agreements * Sending copies to legal or compliance teams * Archiving completed documents with relevant parties ## Signing Order [#signing-order] You can control the sequence in which recipients receive and act on a document by enabling signing order. All recipients receive the document simultaneously and can act in any order. The document is completed when all required recipients have finished their actions. Recipients receive the document one at a time, in the order you specify. Each recipient must complete their action before the next recipient is notified. To enable sequential signing: 1. When adding recipients, check the "Enable signing order" option 2. Assign an order number to each recipient 3. Recipients with the same order number can act simultaneously 4. The document proceeds to the next order number only when all recipients at the current level have completed their actions Sequential signing is required if you want to use the Assistant role. ## Related [#related] * [Add Recipients](/docs/users/documents/add-recipients) - How to add recipients to a document * [Field Types](/docs/concepts/field-types) - Learn about the different field types you can assign to recipients # Signing Certificates ## How Documenso Signs Documents [#how-documenso-signs-documents] Documenso applies a digital signature to the PDF when all recipients complete their actions. {/* prettier-ignore */} ### Create hash [#create-hash] Creates a cryptographic hash of the document content. ### Sign the hash [#sign-the-hash] Signs the hash using the certificate's private key. ### Embed signature [#embed-signature] Embeds the signature and certificate information into the PDF. The signature is applied at the platform level, not by individual signers. Each signer's actions (signature image, text, checkboxes) are recorded and sealed together in the final signed document. ## What the Signature Proves [#what-the-signature-proves] The digital signature provides two guarantees: | Guarantee | Description | | ---------------- | -------------------------------------------------------------------------- | | **Integrity** | The document has not been altered since signing | | **Authenticity** | The document was signed by the certificate holder (the Documenso instance) | If anyone modifies the PDF after signing, the signature becomes invalid. PDF readers will display a warning that the document has been changed. ## Timestamps [#timestamps] Documenso can include a trusted timestamp from a Time Stamping Authority (TSA) in the signature. This proves when the document was signed, independent of the signer's system clock. Timestamps are important for: * Legal evidence of when signing occurred * Long-term validation (LTV) of signatures * Compliance with archival requirements ## Viewing the Signature in PDF Readers [#viewing-the-signature-in-pdf-readers] You can verify a signed document's signature in any PDF reader that supports digital signatures. 1. Open the signed PDF 2. Click the signature panel on the left, or click on a signature field 3. View certificate details, signing time, and validation status Preview, Foxit, and other PDF readers also display signature information, though the interface varies. Look for a signatures or security panel in the application menu. The signature panel shows who signed (certificate subject), when it was signed, whether the document has been modified, and certificate trust status. ## Certificate Trust and Validation [#certificate-trust-and-validation] PDF readers validate signatures against their list of trusted Certificate Authorities (CAs). You may see different validation results depending on the certificate type: | Certificate Type | Validation Result | | ---------------- | ---------------------------------------------------------------------- | | **CA-issued** | Green checkmark in Adobe if the CA is on the Adobe Approved Trust List | | **Self-signed** | Warning that the certificate is not from a trusted source | A self-signed certificate still provides integrity verification. The document cannot be modified without invalidating the signature. The warning only indicates that a third-party CA has not verified the certificate issuer's identity. For most use cases, self-signed certificates are sufficient. The signature still proves the document came from your Documenso instance and has not been tampered with. ## Using Custom Certificates [#using-custom-certificates] If you self-host Documenso, you can use your own signing certificate. Free and suitable for most use cases. The signature still proves document integrity and authenticity. You may see a warning in PDF readers that the certificate is not from a trusted source, but the document cannot be modified without invalidating the signature. Provides trusted validation in PDF readers (e.g. green checkmark in Adobe) when the CA is on the Adobe Approved Trust List. Required for some compliance scenarios where third-party verification of the certificate issuer is needed. See [Signing Certificate Configuration](/docs/self-hosting/configuration/signing-certificate) for setup instructions. ## Related [#related] * [Signature Levels](/docs/compliance/signature-levels) - Simple, Advanced, and Qualified electronic signatures * [Standards and Regulations](/docs/compliance/standards) - ESIGN, eIDAS, and other compliance frameworks * [Signing Certificate Configuration](/docs/self-hosting/configuration/signing-certificate) - Self-hosting certificate setup # Signing Workflow ## Workflow Overview [#workflow-overview] A typical signing workflow follows these steps: 1. **Prepare** - Upload the document, add recipients, and place fields 2. **Send** - Distribute the document to recipients 3. **Notify** - Recipients receive signing requests 4. **Sign** - Recipients complete their assigned fields 5. **Complete** - Document is sealed and distributed to all parties {/* prettier-ignore */} ### Prepare the document [#prepare-the-document] Document preparation involves three main tasks: uploading, adding recipients, and placing fields. **Upload the document** Start by uploading a PDF. You can upload directly: * from your device * create from an existing template * or duplicate a previously sent document. Once uploaded, the document enters the **Draft** state. **Add recipients** Add the people who need to interact with the document. Each recipient needs: * an email address * a name * a role Available roles are: | Role | Purpose | | --------- | --------------------------------------------------- | | Signer | Must sign the document | | Approver | Must approve (signature optional) | | Viewer | Must confirm they viewed the document | | Assistant | Pre-fills fields for other recipients | | CC | Receives a copy after completion (no action needed) | **Place fields** Add fields that recipients will complete. At minimum, each signer needs one signature field. You can also add: * name * email * date * text * number * dropdown * checkbox * radio * initials fields Each field is assigned to a specific recipient, indicated by color coding in the editor. The document cannot be sent until every signer has at least one signature field assigned to them. ### Send the document [#send-the-document] When the document is ready, you send it to recipients. You have two distribution options: Recipients receive an email notification with a link to sign. You can customize the email: * subject line * message body with personalized variables * reply-to address for recipient responses Generate signing links without sending emails. Use this when you want to: * send links via SMS or messaging apps * embed links in your own application * control notification timing yourself After sending, the document moves from **Draft** to **Pending** status. ### Recipients are notified [#recipients-are-notified] When you send a document via email, each recipient receives a notification containing: * the document title * your name and email (or team name) * your custom message (or a role-specific default) * a unique signing link The signing link is specific to each recipient and cannot be used by others. Links remain active until the document is completed, deleted, or expired. **Signing order** By default, all recipients are notified simultaneously (parallel signing). If you enable sequential signing, only recipients in the first signing position receive notifications initially. When they complete their actions, the next group is notified. This continues until all recipients have been notified and completed their actions. ### Recipients sign [#recipients-sign] When a recipient clicks their signing link, they see the document with their assigned fields highlighted. The signing experience depends on their role: Signers must complete all required fields before they can finish. For signature fields, they can: * draw a signature using mouse or touchscreen * type their name and select a font style * upload an image of their existing signature After completing all fields, the signer clicks a button to submit. They receive a confirmation and can download a copy of the document showing their completed fields. Approvers review the document and must explicitly approve it. If signature fields are assigned, they can optionally sign. The document cannot proceed until all approvers have given approval. Viewers see the full document and must confirm they have viewed it. They cannot add signatures or modify any content. **Authentication** You can require recipients to verify their identity before signing through: * email verification (confirm access to the email address) * access code (enter a code you provide separately) * passkey (authenticate with a hardware or software passkey) ### Document is completed [#document-is-completed] Once all recipients with required actions have completed them, the document is finalized. | Aspect | Description | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sealing** | The completed document is sealed with a digital certificate that cryptographically signs the PDF, prevents modification without detection, and provides proof of authenticity. | | **Audit trail** | An audit log is generated and can be attached to the document. It records when the document was created and sent, when each recipient viewed and signed, IP addresses and timestamps for each action, and any authentication methods used. | | **Distribution** | All parties receive the completed document: signers, approvers, and viewers receive their copy via email; CC recipients receive their first notification with the completed document; the document owner can download the signed PDF from their dashboard. | ## Workflow Variations [#workflow-variations] Documenso supports several workflow variations to handle different signing scenarios. When recipients must sign in a specific order, enable signing order: 1. Assign each recipient a signing position (1, 2, 3, etc.) 2. Recipients at position 1 sign first 3. Recipients at position 2 are notified only after position 1 completes 4. Multiple recipients can share the same position to sign in parallel within that step Use sequential signing when later signers: * need to see what earlier signers entered * approval must happen before final signatures * company policy requires a specific signing order Combine approver and signer roles to create approval workflows: 1. Add approvers at signing position 1 2. Add signers at signing position 2 3. Approvers review and approve first 4. Signers are notified only after approval is complete If an approver rejects the document (when rejection is enabled), the workflow stops and signers are never notified. Use assistants to have one person prepare the document for another: 1. Add an assistant at signing position 1 2. Add the final signer at signing position 2 3. The assistant pre-fills fields with suggested values 4. The signer reviews and completes their signature This is useful when administrative staff prepare documents for executives or when gathering information from one person while another signs. The Assistant role is only available when sequential signing is enabled. For high-volume signing scenarios, you can create direct links that allow anyone to sign without receiving an individual invitation: * Generate a public signing link for a document or template * Share the link on your website, in emails, or through other channels * Each person who accesses the link creates their own signing instance * Useful for waivers, consent forms, and public agreements ## Related Concepts [#related-concepts] * [Document Lifecycle](/docs/concepts/document-lifecycle) - Understanding document states from draft to completion * [Recipient Roles](/docs/concepts/recipient-roles) - Detailed explanation of each role type * [Field Types](/docs/concepts/field-types) - All available field types and their configuration options * [Signing Certificates](/docs/concepts/signing-certificates) - How documents are digitally sealed # Certifications & Regulatory Compliance ### Compliance Status Overview [#compliance-status-overview] | Certification | Status | | -------------- | ---------------------- | | 21 CFR Part 11 | Compliant (Enterprise) | | SOC 2 | Compliant | | ISO 27001 | Planned | | HIPAA | Compliant (Enterprise) | ## 21 CFR Part 11 [#21-cfr-part-11] Status: Compliant (Enterprise License) 21 CFR Part 11 is a regulation by the FDA that establishes the criteria for electronic records and electronic signatures to ensure their authenticity, integrity, and confidentiality in the pharmaceutical, medical device, and other FDA-regulated industries. Read more about [21 CFR Part 11 with Documenso](https://documen.so/21-CFR-Part-11). ### Main Requirements [#main-requirements] * Strong Identity Checks for each Signature * Signature and Audit Trails * User Access Management * Quality Assurance Documentation ## SOC 2 [#soc-2] Status: [Compliant](https://documen.so/trust) SOC 2 is a framework for managing and auditing the security, availability, processing integrity, confidentiality, and data privacy in cloud and IT service organizations, established by the American Institute of Certified Public Accountants (AICPA). ## ISO 27001 [#iso-27001] Status: [Planned](https://github.com/documenso/backlog/issues/26) ISO 27001 is an international standard for managing information security, specifying requirements for establishing, implementing, maintaining, and continually improving an information security management system (ISMS). ## HIPAA [#hipaa] Status: [Compliant](https://documen.so/trust) The HIPAA (Health Insurance Portability and Accountability Act) is a U.S. law designed to protect patient health information's privacy and security and improve the healthcare system's efficiency and effectiveness. *** ## See Also [#see-also] * [Standards](/docs/compliance/standards) - Technical signing standards (PDF/A, PAdES, X.509) * [Signature Levels](/docs/compliance/signature-levels) - eIDAS and other signature level compliance * [Enterprise Edition](/docs/policies/enterprise-edition) - Enterprise licensing for compliance features * [GDPR](/docs/compliance/gdpr) - Data protection compliance # E-Sign Compliance ## ESIGN Act (United States) [#esign-act-united-states] The Electronic Signatures in Global and National Commerce Act (ESIGN Act) is a U.S. federal law enacted in 2000. It ensures that electronic signatures and records have the same legal validity as paper documents and handwritten signatures in interstate and foreign commerce. ### Key Requirements [#key-requirements] | Requirement | Description | | ----------------------- | ----------------------------------------------------------------------------------------- | | **Intent to Sign** | Signers must demonstrate clear intent to sign the document | | **Consent** | All parties must agree to conduct the transaction electronically | | **Consumer Disclosure** | For consumer transactions, specific disclosures must be provided before obtaining consent | | **Record Retention** | Electronic records must be accurately preserved and accessible for later reference | | **Association** | The signature must be associated with the record being signed | ### Exclusions [#exclusions] The ESIGN Act does not apply to certain document types, including: * Wills, codicils, and testamentary trusts * Family law documents (adoption, divorce) * Court orders and official court documents * Cancellation of utility services * Documents related to hazardous materials transportation *** ## UETA (United States) [#ueta-united-states] The Uniform Electronic Transactions Act (UETA) is a model law adopted by 49 U.S. states (all except New York, which has its own Electronic Signatures and Records Act). UETA provides a legal framework for electronic signatures and records at the state level. ### Relationship to ESIGN [#relationship-to-esign] UETA and the ESIGN Act have similar requirements and purposes. The federal ESIGN Act allows states to modify or supersede certain ESIGN provisions if they adopt UETA or an equivalent law. In practice, the requirements for electronic signatures under both laws align closely. ### Key Requirements [#key-requirements-1] * Intent to sign demonstrated by the signer * Consent to conduct transactions electronically * Retention of records in their original electronic form * Attribution of the signature to the signer *** ## eIDAS (European Union) [#eidas-european-union] The Electronic Identification, Authentication and Trust Services (eIDAS) regulation governs electronic signatures across all EU member states. eIDAS establishes three levels of electronic signatures, each with different requirements and legal effects. ### Signature Levels [#signature-levels] | Level | Description | Legal Effect | | ------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------- | | **Simple (SES)** | Basic electronic signature with no specific technical requirements | Admissible as evidence; legal effect varies by use | | **Advanced (AES)** | Uniquely linked to signer, capable of identifying signer, under sole control | Higher evidentiary weight than SES | | **Qualified (QES)** | AES created by a qualified signature creation device, based on a qualified certificate | Equivalent to handwritten signature across the EU | ### Simple Electronic Signatures (SES) [#simple-electronic-signatures-ses] SES is the baseline level. Any data in electronic form attached to or logically associated with other electronic data, used by the signatory to sign, qualifies as an SES. There are no specific technical requirements beyond demonstrating intent to sign. ### Advanced Electronic Signatures (AES) [#advanced-electronic-signatures-aes] AES must meet additional criteria: * Uniquely linked to the signatory * Capable of identifying the signatory * Created using signature creation data under the signatory's sole control * Linked to the signed data in a way that detects subsequent changes ### Qualified Electronic Signatures (QES) [#qualified-electronic-signatures-qes] QES requires: * A qualified certificate issued by a qualified trust service provider * Creation using a qualified electronic signature creation device * Identity verification compliant with eIDAS requirements QES carries the same legal standing as a handwritten signature in all EU member states. *** ## Other Jurisdictions [#other-jurisdictions] Electronic signature laws exist in most countries. Below are selected examples: | Jurisdiction | Framework | Notes | | ------------------ | --------------------------------------------- | ------------------------------------------------------------ | | **United Kingdom** | UK eIDAS / Electronic Communications Act 2000 | Post-Brexit, UK maintains eIDAS-like framework | | **Canada** | PIPEDA, provincial laws | Federal and provincial laws govern e-signatures | | **Australia** | Electronic Transactions Act 1999 | Generally technology-neutral approach | | **Switzerland** | ZertES | Swiss federal law with qualified signature requirements | | **Brazil** | MP 2200-2, ICP-Brasil | PKI-based framework for digital signatures | | **India** | IT Act 2000, Aadhaar e-KYC | Recognizes electronic signatures; Aadhaar-based verification | | **China** | Electronic Signature Law | Requires reliable electronic signatures for certain uses | | **Japan** | Electronic Signatures Act | Three-tier system similar to eIDAS | Requirements vary significantly by jurisdiction. Some transactions may require specific signature types or have exclusions similar to the ESIGN Act. *** ## How Documenso Supports Compliance [#how-documenso-supports-compliance] Documenso provides features that support compliance with e-signature laws across jurisdictions: ### Intent to Sign [#intent-to-sign] * Signers must actively interact with signature fields to apply their signature * The signing interface clearly indicates the document being signed * Signers receive a copy of the completed document ### Consent [#consent] * Recipients receive clear notification that they are being asked to sign electronically * The signing process requires affirmative action from the signer ### Record Retention [#record-retention] * Signed documents are stored and accessible to all parties * Original documents and audit trails are preserved * Documents can be downloaded in their signed form at any time ### Document Integrity [#document-integrity] * All completed documents are cryptographically sealed * Any modification after signing invalidates the digital signature * PDF readers can verify the document has not been altered ### Signer Identification [#signer-identification] * Email-based delivery establishes signer identity * Optional access codes add verification * Signing activity is logged with timestamps and metadata *** ## Audit Trails [#audit-trails] Documenso maintains an audit trail for each document, recording: | Event | Recorded Data | | ------------------ | -------------------------------------- | | Document creation | Timestamp, creator identity | | Recipient addition | Recipient details, assigned fields | | Document sent | Timestamp, delivery method | | Document viewed | Timestamp, viewer identity, IP address | | Field completed | Timestamp, field type, signer identity | | Document completed | Timestamp, final document hash | The audit trail provides evidence of the signing process, including who signed, when they signed, and the sequence of events. This information supports the legal enforceability of the signed document. *** ## What Documenso Does NOT Provide [#what-documenso-does-not-provide] Documenso supports compliance with Simple Electronic Signature (SES) requirements. The following are not currently provided: | Capability | Status | | ----------------------------------------- | ------------------------------------------------------------------------------------- | | **Qualified Electronic Signatures (QES)** | Not supported; requires integration with qualified trust service providers | | **Advanced Electronic Signatures (AES)** | Partial support; full AES requires identity verification services | | **Identity Verification (KYC)** | Not built-in; optional integrations may be available | | **Qualified Certificates** | Not issued; would require becoming a qualified trust service provider | | **Industry-Specific Compliance** | Features for specific regulations (e.g., healthcare, finance) depend on configuration | For transactions requiring AES or QES, consult with legal counsel about appropriate solutions. *** ## Disclaimer [#disclaimer] This page provides general information about electronic signature laws for educational purposes. It does not constitute legal advice. Electronic signature requirements vary by jurisdiction, transaction type, and specific circumstances. Some documents may have specific legal requirements that electronic signatures cannot satisfy. Consult qualified legal counsel in your jurisdiction to determine whether electronic signatures are appropriate for your specific use case and what requirements must be met. *** ## Related [#related] * [Signature Levels](/docs/compliance/signature-levels) - Simple, Advanced, and Qualified electronic signatures explained * [Standards & Regulations](/docs/compliance/standards) - SOC 2, 21 CFR Part 11, and other compliance frameworks * [Signing Certificates](/docs/concepts/signing-certificates) - How documents are digitally signed and verified # GDPR ## Documenso's Role [#documensos-role] When using Documenso for document signing, two distinct data processing roles apply: | Role | Description | | ------------------- | ------------------------------------------------------------------------------------- | | **Data Controller** | You (the organisation using Documenso) determine the purposes and means of processing | | **Data Processor** | Documenso processes personal data on your behalf according to your instructions | As the data controller, you are responsible for: * Obtaining appropriate consent or legal basis for processing * Informing data subjects about how their data is used * Responding to data subject access requests * Ensuring compliance with GDPR requirements As the data processor, Documenso: * Processes data only according to your instructions * Implements appropriate security measures * Assists with data subject requests when needed * Maintains records of processing activities ## Data Processing [#data-processing] Documenso processes personal data necessary to provide document signing services: | Data Category | Examples | Purpose | | ------------------ | ---------------------------------------------- | --------------------------------------- | | **Identity Data** | Name, email address | User accounts, recipient identification | | **Document Data** | Uploaded PDFs, field values | Document storage and signing | | **Signature Data** | Signature images, signing timestamps | Recording signing actions | | **Audit Data** | IP addresses, browser information, action logs | Audit trail and verification | Data is processed for the following purposes: * Delivering documents to recipients * Recording signatures and other recipient actions * Generating signed documents with audit trails * Sending email notifications ## Data Storage Locations [#data-storage-locations] Where your data is stored depends on how you use Documenso: For the hosted cloud service: * Application data is stored in data centres within the European Union * Document storage uses EU-based infrastructure * Backups are maintained in geographically separate EU locations Contact Documenso for specific information about sub-processors and data centre locations. When you self-host Documenso: * You control all data storage locations * No data is transmitted to Documenso's infrastructure * You choose your own database, file storage, and backup locations Self-hosting provides complete control over data residency, which may be required for certain compliance scenarios. ## Data Subject Rights [#data-subject-rights] GDPR grants individuals specific rights regarding their personal data. As the data controller, you are responsible for fulfilling these requests: | Right | Description | | ----------------- | -------------------------------------------------------------------------- | | **Access** | Data subjects can request a copy of their personal data | | **Rectification** | Data subjects can request correction of inaccurate data | | **Erasure** | Data subjects can request deletion of their data ("right to be forgotten") | | **Portability** | Data subjects can request their data in a machine-readable format | | **Restriction** | Data subjects can request limited processing of their data | | **Objection** | Data subjects can object to certain types of processing | When you receive a data subject request, you can: * Export user and document data from your Documenso account * Delete user accounts and associated documents * Contact Documenso support for assistance with cloud-hosted data ## Data Deletion [#data-deletion] Documenso supports data deletion to help fulfill erasure requests: * Users can delete their own accounts * Account deletion removes profile data and authentication credentials * Team owners can remove members from teams * Document owners can delete documents in draft state * Completed documents can be deleted by the owner * Deletion removes the document, recipient data, and associated audit logs For signed documents, you may need to balance deletion requests against: * Legal requirements to retain signed contracts * Your organisation's record-keeping policies * The rights of other parties to the signed document Consult with legal counsel to establish appropriate retention policies. ## Self-Hosting for GDPR Compliance [#self-hosting-for-gdpr-compliance] Self-hosting Documenso can simplify GDPR compliance: * **Data residency** - Store all data in your chosen jurisdiction * **Sub-processor control** - No third-party data processors beyond your own infrastructure * **Direct access** - Full database access for data subject requests * **Retention control** - Implement custom data retention and deletion policies See the [Self-Hosting Guide](/docs/self-hosting) for deployment options. ## Data Processing Agreement [#data-processing-agreement] A Data Processing Agreement (DPA) is a contract required by GDPR when a data controller engages a data processor. * A DPA is available upon request * Contact [support@documenso.com](mailto:support@documenso.com) to request a DPA * The DPA covers Documenso's obligations as a data processor No DPA with Documenso is required since no personal data is processed by Documenso. *** ## Disclaimer [#disclaimer] This documentation is provided for informational purposes only and does not constitute legal advice. GDPR compliance depends on your specific circumstances, including how you use Documenso, what data you process, and your organisation's obligations. Consult with qualified legal counsel to: * Determine your GDPR obligations * Draft appropriate privacy notices * Establish lawful bases for processing * Implement compliant data handling procedures *** ## Related [#related] * [Standards & Regulations](/docs/compliance/standards) - eIDAS, ESIGN Act, and other compliance frameworks * [Self-Hosting Guide](/docs/self-hosting) - Deploy Documenso on your own infrastructure * [Security Settings](/docs/users/settings/security) - Configure authentication and security options # Compliance ## Overview [#overview] ## Additional Topics [#additional-topics] ## Disclaimer [#disclaimer] This documentation is provided for informational purposes only. It does not constitute legal advice and should not be relied upon as such. Compliance requirements vary based on: * Your jurisdiction and applicable laws * The type of documents being signed * Industry-specific regulations * The parties involved in the transaction Consult with qualified legal counsel to determine the specific requirements for your use case. ## Related [#related] * [Privacy Policy](/docs/policies/privacy) - How Documenso handles personal data * [Security](/docs/policies/security) - Security practices and measures * [Terms of Service](/docs/policies/terms) - Terms governing use of Documenso # Signature Levels Documenso seals all signed documents cryptographically, regardless of signature level, to prevent any alterations after signing. ### Compliance Status Overview [#compliance-status-overview] | Regulation | Status | | ------------ | --------- | | ESIGN / UETA | Compliant | | eIDAS SES | Compliant | | eIDAS AES | Planned | | eIDAS QES | Planned | | ZertES | Planned | ## U.S. ESIGN Act [#us-esign-act] Status: Compliant The Electronic Signatures in Global and National Commerce Act (ESIGN Act) is a U.S. federal law that ensures the legal validity and enforceability of electronic signatures and records in commerce. ### Main Requirements [#main-requirements] * **Intent to Sign** - Parties must demonstrate their intent to sign * **Consent** - All parties must consent to the use of electronic signatures and records * **Consumer Disclosures** - Financial institutions must provide clear statements informing consumers before obtaining consent * **Record Retention** - Electronic records must be maintained for later access by signers * **Security** - Parties must take reasonable steps to ensure the security and integrity of electronic signatures and records ## UETA (Uniform Electronic Transactions Act) [#ueta-uniform-electronic-transactions-act] Status: Compliant The Uniform Electronic Transactions Act provides a legal framework for the use of electronic signatures and records in electronic transactions, ensuring they have the same validity and enforceability as paper documents and handwritten signatures. UETA shares the same core requirements as the [ESIGN Act](#us-esign-act). ## Simple Electronic Signatures (SES) [#simple-electronic-signatures-ses] A Simple Electronic Signature is the most basic form of electronic signature. It includes any data in electronic form that is attached to or logically associated with other electronic data and used by the signatory to sign. ### Characteristics [#characteristics] | Aspect | Description | | -------------------------- | ------------------------------------------------------------------------------------- | | **Technical Requirements** | No specific technical requirements beyond demonstrating intent to sign | | **Identity Verification** | None required; relies on email delivery or other indirect identification | | **Legal Status** | Admissible as evidence; cannot be denied legal effect solely because it is electronic | | **Examples** | Typed name, scanned signature image, checkbox acceptance, click-to-sign | ### When SES Is Appropriate [#when-ses-is-appropriate] SES is suitable for many common business transactions: * Standard contracts and agreements * Internal approvals and sign-offs * Terms of service acceptance * Non-disclosure agreements * Purchase orders and invoices * Employment documents (in most jurisdictions) The legal validity of SES depends on the specific transaction and jurisdiction. Many everyday business documents do not require higher signature levels. *** ## Advanced Electronic Signatures (AES) [#advanced-electronic-signatures-aes] An Advanced Electronic Signature meets additional technical and procedural requirements that provide stronger evidence of the signer's identity and the document's integrity. ### Requirements [#requirements] Under eIDAS, an AES must satisfy four criteria: 1. **Uniquely linked to the signatory** - The signature is associated with a specific individual 2. **Capable of identifying the signatory** - The signature data reveals who signed 3. **Created using signature creation data under the signatory's sole control** - Only the signer can create the signature (e.g., private key, secure device) 4. **Linked to the data in such a way that any subsequent change is detectable** - Tampering invalidates the signature ### Characteristics [#characteristics-1] | Aspect | Description | | -------------------------- | -------------------------------------------------------------------------- | | **Technical Requirements** | Cryptographic signature with signer identification | | **Identity Verification** | Required; must establish signer identity through verification process | | **Legal Status** | Higher evidentiary weight than SES; stronger presumption of validity | | **Implementation** | Typically requires identity verification service and personal certificates | ### Compliance Status [#compliance-status] Status: [Planned](https://github.com/documenso/backlog/issues/9) via third party until [Let's Sign](https://github.com/documenso/backlog/issues/21) is realized. Current AES progress: * Cryptographic signature sealing the document against tampering * Signing using dedicated hardware (Hardware Security Module) * Embedding signer identity in the cryptographic signature (planned) * Being a government-audited trusted qualified services provider (planned) ### When AES Is Appropriate [#when-aes-is-appropriate] AES is used when stronger proof of identity and intent is needed: * Financial services agreements * Real estate transactions (in some jurisdictions) * Healthcare consent forms * Government submissions * High-value contracts * Cross-border agreements within the EU *** ## Qualified Electronic Signatures (QES) [#qualified-electronic-signatures-qes] A Qualified Electronic Signature is the highest level of electronic signature under eIDAS. It is legally equivalent to a handwritten signature in all EU member states and carries a presumption of validity. ### Requirements [#requirements-1] QES must meet all AES requirements plus: 1. **Qualified Certificate** - Issued by a Qualified Trust Service Provider (QTSP) that is accredited by an EU member state 2. **Qualified Electronic Signature Creation Device (QSCD)** - The signature is created using hardware or software that meets specific security standards 3. **Identity Verification** - In-person or equivalent remote verification compliant with eIDAS requirements ### Characteristics [#characteristics-2] | Aspect | Description | | -------------------------- | --------------------------------------------------------------------- | | **Technical Requirements** | Qualified certificate + qualified signature creation device | | **Identity Verification** | Strict verification by a Qualified Trust Service Provider | | **Legal Status** | Equivalent to handwritten signature across all EU member states | | **Implementation** | Requires integration with a QTSP; typically involves external service | ### Compliance Status [#compliance-status-1] Status: [Planned](https://github.com/documenso/backlog/issues/32) via third party until [Let's Sign](https://github.com/documenso/backlog/issues/21) is realized. ### When QES Is Required [#when-qes-is-required] Certain transactions require or benefit from QES: * Documents that legally require a handwritten signature under national law * Court filings and legal documents * Company formation documents * Land registry transactions * Notarized documents * Regulated financial transactions * Cross-border transactions requiring guaranteed recognition *** ## Comparison of Signature Levels [#comparison-of-signature-levels] | Aspect | SES | AES | QES | | ------------------------- | ------------ | ------------------------ | --------------------- | | **Technical Complexity** | Low | Medium | High | | **Identity Verification** | None | Required | Strict (QTSP) | | **Legal Effect (EU)** | Admissible | Higher evidentiary value | Equal to handwritten | | **Cost** | Low | Medium | Higher | | **User Experience** | Simple | More steps | Most steps | | **Signer Requirements** | Email access | Identity verification | Certificate from QTSP | ### Legal Recognition [#legal-recognition] | Jurisdiction | SES | AES | QES | | ------------------ | ------------------------------- | -------------------------- | ------------------------------------ | | **European Union** | Valid, evidentiary value varies | Enhanced evidentiary value | Equivalent to handwritten | | **United States** | Valid under ESIGN/UETA | No formal distinction | No formal distinction | | **United Kingdom** | Valid | Enhanced value | Equivalent to handwritten (UK eIDAS) | | **Switzerland** | Valid | Valid | Equivalent to handwritten (ZertES) | *** ## What Documenso Provides [#what-documenso-provides] Documenso supports Simple Electronic Signatures (SES) with features that enhance evidentiary value: ### SES Features [#ses-features] * **Intent to Sign** - Signers actively interact with signature fields * **Email-Based Delivery** - Documents sent to specific email addresses * **Audit Trail** - Complete record of signing events, timestamps, and IP addresses * **Document Integrity** - Cryptographic sealing detects any post-signing modifications * **Record Retention** - Signed documents stored and accessible to all parties ### Additional Verification Options [#additional-verification-options] * **Access Codes** - Require signers to enter a code before accessing documents * **Signing Order** - Control the sequence of signatures ### What Documenso Does Not Provide [#what-documenso-does-not-provide] | Capability | Status | | ----------------------------------------- | --------------------------------------------------------- | | **Qualified Electronic Signatures (QES)** | Not supported; requires QTSP integration | | **Advanced Electronic Signatures (AES)** | Partial; full AES requires identity verification services | | **Identity Verification (KYC)** | Not built-in | | **Qualified Certificates** | Not issued; would require QTSP status | For transactions requiring AES or QES, you would need to integrate with external identity verification services or Qualified Trust Service Providers. *** ## ZertES (Swiss Federal Law) [#zertes-swiss-federal-law] Status: [Planned](https://github.com/documenso/backlog/issues/34) ZertES is a Swiss federal law that regulates electronic signature compliance. It defines requirements similar to eIDAS for qualified electronic signatures within Switzerland. *** ## When You Need Higher Signature Levels [#when-you-need-higher-signature-levels] Consider using AES or QES when: * National law requires a handwritten signature (QES may substitute) * Regulations specify signature requirements (e.g., certain financial or healthcare documents) * Cross-border enforceability is critical * High contract value or significant liability * Higher likelihood of disputes * Need for stronger non-repudiation * Counterparty or regulatory requirements specify higher levels * Financial services with regulatory oversight * Healthcare with patient consent requirements * Government or public sector contracts * Real estate transactions in regulated markets Most business transactions do not require AES or QES. Consider: 1. What does your jurisdiction require for this document type? 2. What do your counterparties or customers expect? 3. What is the risk if the signature is disputed? 4. Does your industry have specific requirements? When in doubt, consult with legal counsel to determine the appropriate signature level for your specific use case. *** ## Disclaimer [#disclaimer] This documentation is provided for informational purposes only and does not constitute legal advice. The appropriate signature level for your documents depends on: * Your jurisdiction and applicable laws * The type of document being signed * Industry-specific regulations * Contractual requirements from counterparties * Risk tolerance and dispute likelihood Electronic signature requirements vary significantly across jurisdictions and document types. Some transactions have specific legal requirements that may mandate particular signature levels or exclude electronic signatures entirely. Consult with qualified legal counsel to determine the signature level requirements for your specific use case. *** ## Related [#related] * [E-Sign Compliance](/docs/compliance/esign) - ESIGN Act, UETA, eIDAS, and electronic signature laws * [Signing Certificates](/docs/concepts/signing-certificates) - How documents are digitally signed and verified * [Standards & Regulations](/docs/compliance/standards) - SOC 2, 21 CFR Part 11, and other frameworks # Standards & Regulations ## PDF/A for Archival [#pdfa-for-archival] PDF/A is an ISO-standardized version of PDF designed for long-term archival of electronic documents. Unlike standard PDFs, PDF/A files are self-contained and do not rely on external resources. Key characteristics: * All fonts must be embedded * No external content references allowed * No encryption that would prevent future access * Metadata must be embedded in XMP format * Color spaces must be device-independent or include ICC profiles PDF/A has several conformance levels (PDF/A-1, PDF/A-2, PDF/A-3) with increasing capabilities. PDF/A-3, for example, allows embedding of arbitrary file formats as attachments. For signed documents intended for long-term storage, PDF/A ensures the document remains readable and verifiable years or decades after signing. ## PAdES (PDF Advanced Electronic Signatures) [#pades-pdf-advanced-electronic-signatures] PAdES is a set of standards (ETSI EN 319 142) that defines profiles for electronic signatures in PDF documents. It builds on the PDF signature capabilities defined in ISO 32000 and adds requirements for long-term validity. PAdES defines several signature profiles: | Profile | Description | | --------- | ---------------------------------------------------- | | PAdES-B | Basic signature with signing certificate | | PAdES-T | Adds a trusted timestamp | | PAdES-LT | Adds validation data (certificates, revocation info) | | PAdES-LTA | Adds long-term archival timestamps | Each level builds upon the previous, with PAdES-LTA providing the strongest guarantees for long-term signature validity. The inclusion of validation data and archival timestamps allows signatures to be verified even after certificates expire or CAs cease operations. ## ISO 32000 (PDF Standard) [#iso-32000-pdf-standard] ISO 32000 is the international standard that defines the PDF format. It specifies the technical foundation for digital signatures in PDF documents. Relevant signature capabilities defined in ISO 32000: * Signature field dictionaries and appearance streams * Cryptographic signature handlers * Certificate and timestamp embedding * Incremental updates for signature preservation * Document modification detection ISO 32000-2 (PDF 2.0) introduced additional features including support for more signature algorithms and improved encryption options. ## X.509 Certificates [#x509-certificates] X.509 is the standard format for public key certificates used in digital signatures. These certificates bind a public key to an identity and are issued by Certificate Authorities (CAs). A typical X.509 certificate contains: * Subject (identity information) * Issuer (the CA that issued the certificate) * Public key * Validity period (not before / not after dates) * Serial number * Signature algorithm * Extensions (key usage, policies, etc.) For document signing, certificates typically include the "digital signature" key usage extension. Qualified certificates under eIDAS regulations have additional requirements and provide higher levels of assurance. Certificate validation involves checking: 1. The certificate chain up to a trusted root CA 2. That no certificate in the chain has expired 3. Revocation status via CRL or OCSP ## RFC 3161 (Timestamping) [#rfc-3161-timestamping] RFC 3161 defines the Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP). Timestamps prove that a document existed in a specific state at a particular point in time. A timestamp token contains: * Hash of the signed data * Time of issuance (from a trusted time source) * Identifier of the Time Stamping Authority (TSA) * TSA's digital signature Timestamps serve two purposes in document signing: 1. **Proof of existence**: Demonstrates the document was signed before a certain time 2. **Signature validity extension**: Allows signature verification after the signing certificate expires Without a trusted timestamp, a signature can only be verified while the signing certificate remains valid. With a timestamp, the signature remains verifiable as long as the timestamp can be validated. ## What Documenso Implements [#what-documenso-implements] Documenso implements digital signatures with the following characteristics: * **PDF signatures**: Documents are signed using the PDF signature capabilities defined in ISO 32000 * **X.509 certificates**: Signatures use X.509 certificates for signer identification * **Timestamps**: RFC 3161 timestamps can be applied to signatures * **Signature visualization**: Signed documents include visual signature representations For specific implementation details and configuration options, refer to the [signing certificates](/docs/concepts/signing-certificates) documentation. Self-hosted deployments can configure their own signing certificates and timestamp authorities to meet specific compliance requirements. ## Related [#related] * [E-Sign Compliance](/docs/compliance/esign) - Legal frameworks for electronic signatures * [Signing Certificates](/docs/concepts/signing-certificates) - Certificate configuration * [Signing Workflow](/docs/concepts/signing-workflow) - Document activity and audit trail # Developer Guide ## Getting Started [#getting-started] *** ## Integration Options [#integration-options] *** ## API Base URL [#api-base-url] ``` https://app.documenso.com/api/v2 ``` For self-hosted instances, replace with your instance URL: ``` https://your-instance.com/api/v2 ``` *** ## SDKs [#sdks] Official SDKs are available for multiple languages: * [TypeScript SDK](https://github.com/documenso/sdk-typescript) * [Python SDK](https://github.com/documenso/sdk-python) * [Go SDK](https://github.com/documenso/sdk-go) *** ## Looking for Something Else? [#looking-for-something-else] # Community Edition ## AGPL-3.0 License Overview [#agpl-30-license-overview] The GNU Affero General Public License version 3 (AGPL-3.0) is a copyleft license designed to ensure software freedom. It is similar to GPL-3.0 but includes an additional "network clause" that addresses software-as-a-service use cases. Key characteristics: * **Copyleft**: Derivative works must use the same license * **Network clause**: Network access triggers the same obligations as distribution * **Source availability**: Users must be able to access the source code The full license text is available in the [LICENSE file](https://github.com/documenso/documenso/blob/main/LICENSE) in the repository. ## What You Can Do [#what-you-can-do] Under the AGPL-3.0 license, you are permitted to: | Action | Description | | ------------------ | ------------------------------------------------------------- | | **Use** | Run Documenso for any purpose, including commercial use | | **Study** | Access and examine the complete source code | | **Modify** | Make changes and customizations to the software | | **Distribute** | Share copies of the original or modified software | | **Self-host** | Deploy on your own servers, cloud infrastructure, or locally | | **Offer services** | Provide document signing services to users (with obligations) | ## What You Must Do [#what-you-must-do] When using or distributing the Community Edition, you must comply with these requirements: If you modify Documenso and make it available over a network, you must: * Provide access to the complete source code of your modified version * Make the source available through the same network interface, or provide a written offer to supply it * Include all scripts and instructions needed to build and install the software The network clause is the distinguishing feature of AGPL-3.0. If users interact with your modified Documenso instance over a network (whether internal or external), you must provide them access to the source code. You must maintain proper attribution: * Keep all copyright notices intact in the source code * Include a copy of the AGPL-3.0 license with any distribution * Clearly state any modifications you have made * Preserve author attributions and license headers Derivative works must be licensed under AGPL-3.0: * Forks must use the same license * Modifications cannot be made proprietary * Combined works that incorporate Documenso code are subject to AGPL-3.0 Features located in the `packages/ee/` directory of the codebase are not licensed under AGPL-3.0 and require an active [Enterprise license](/docs/policies/enterprise-edition) to use. If you are self-hosting the Community Edition, do not enable or use features from the `packages/ee/` folder without an enterprise license. ## Features Included [#features-included] The Community Edition includes the core Documenso functionality: * Upload and prepare PDF documents for signing * Add multiple recipients with different roles (signer, approver, viewer, etc.) * Configure signing order and workflow * Track document status and completion * Signature fields (draw, type, or upload) * Initial fields * Text fields * Date fields * Checkbox fields * Number fields * Dropdown/select fields * Radio button fields * Email-based signing workflow * Direct signing links * Signing certificate integration * Completed document download * Create reusable document templates * Pre-configure recipients and fields * Generate documents from templates via UI or API * REST API for document operations * Webhook notifications for events * API token authentication * Docker and Docker Compose deployment * Kubernetes deployment option * Railway one-click deployment * PostgreSQL database support * S3-compatible storage support * SMTP email configuration For the complete list of configuration options, see the [Self-Hosting Guide](/docs/self-hosting). ## Support Options [#support-options] Community Edition users have access to community-based support: The Documenso Discord server is the primary community support channel. You can: * Ask questions and get help from other users * Share your use cases and solutions * Report issues and discuss features * **[GitHub Discussions](https://github.com/documenso/documenso/discussions)**: For questions, ideas, and general discussion * **[GitHub Issues](https://github.com/documenso/documenso/issues)**: For confirmed bug reports with reproducible steps * [Self-Hosting Guide](/docs/self-hosting) for deployment and configuration * [Developer Guide](/docs/developers) for API integration * [Troubleshooting Guide](/docs/self-hosting/maintenance/troubleshooting) for common issues Community support is provided on a best-effort basis by volunteers and Documenso team members. For guaranteed response times and dedicated support, consider the Enterprise Edition. ## Contributing Back [#contributing-back] Contributions to Documenso are welcome and appreciated. You can contribute in several ways: 1. Fork the [Documenso repository](https://github.com/documenso/documenso) 2. Create a branch for your changes 3. Submit a pull request with a clear description 4. Respond to code review feedback All contributions are licensed under AGPL-3.0. * Report bugs with detailed reproduction steps * Suggest features through GitHub Discussions * Improve documentation * Help other users in Discord or GitHub Discussions * Translate the interface to other languages Before contributing, review: * The [CONTRIBUTING.md](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md) file in the repository * Existing issues and discussions to avoid duplicates * The coding standards used in the project ## When to Consider Enterprise Edition [#when-to-consider-enterprise-edition] The Community Edition is appropriate for many use cases. However, you may need the Enterprise Edition if you: * Cannot comply with AGPL-3.0 source disclosure requirements * Need to keep modifications proprietary * Require dedicated support with guaranteed response times * Need enterprise-specific features (SSO, advanced audit logs, etc.) * Have corporate policies that prohibit AGPL-licensed software See [Enterprise Edition](/docs/policies/enterprise-edition) for details on commercial licensing. ## Related [#related] * [Licenses](/docs/policies/licenses) - Overview of both licensing options * [Licenses](/docs/policies/licenses) - Detailed AGPL-3.0 explanation * [Enterprise Edition](/docs/policies/enterprise-edition) - Commercial license details * [Self-Hosting Guide](/docs/self-hosting) - Deploy your own instance * [Support](/docs/policies/support) - All support options * [Full AGPL-3.0 License Text](https://www.gnu.org/licenses/agpl-3.0.html) - Complete license terms * [GitHub Repository](https://github.com/documenso/documenso) - Source code and LICENSE file # Enterprise Edition ## When You Need Enterprise [#when-you-need-enterprise] The Enterprise Edition is required when you: * **Build proprietary products**: Integrate Documenso into commercial software without releasing source code * **Offer SaaS with modifications**: Provide document signing as part of your platform with custom modifications you want to keep private * **Cannot comply with AGPL-3.0**: Corporate policies or legal requirements prohibit use of AGPL-licensed software * **Need to keep modifications private**: Any changes to Documenso source code that you don't want to disclose If you run Documenso without modifications, or your modifications are open source under AGPL-3.0, the Community Edition is sufficient. ### Common Use Cases [#common-use-cases] | Scenario | License Needed | | -------------------------------------------- | ------------------ | | Internal use without modifications | Community Edition | | Internal use with private modifications | Enterprise Edition | | SaaS product using Documenso API only | Community Edition | | SaaS product with modified Documenso code | Enterprise Edition | | White-label product with proprietary changes | Enterprise Edition | | Open-source project under AGPL-3.0 | Community Edition | ## Features and Benefits [#features-and-benefits] The Enterprise license grants: * **No source disclosure**: Keep your modifications private. No obligation to share code with users or the public. * **Proprietary integration**: Embed Documenso in closed-source products without licensing conflicts. * **Sublicensing options**: Distribute Documenso as part of your product to your customers (terms vary by agreement). * **Perpetual use**: License terms that allow continued use of the licensed version. Enterprise licenses include access to features not available in the Community Edition: * Stripe Billing Module * Organisation Authentication Portal (SSO with SAML and OIDC) * Document Action Reauthentication (Passkeys and 2FA) * 21 CFR Part 11 Compliance * Email Domains (custom sender addresses) * Embed Editor * Embed Editor White Label * Custom signing certificates * Priority feature requests The canonical list of enterprise-gated features is maintained in the `packages/ee/FEATURES` file in the codebase. Features in the `packages/ee/` directory require an active Enterprise license. 1. Acquire a license by contacting [sales](https://documen.so/enterprise) 2. Access your license key at [license.documenso.com](https://license.documenso.com) 3. Set the environment variable: ```bash NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY="your-license-key-here" ``` 4. Restart your Documenso instance 5. Verify the license is active in the **Admin Panel** under the **Stats** section See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed. ## Licensing Terms [#licensing-terms] The Enterprise license is a commercial agreement between your organisation and Documenso. Key terms include: * **Scope**: License to use, modify, and distribute Documenso as specified in your agreement * **Exclusions**: The license does not grant rights to Documenso trademarks beyond what's specified * **Termination**: License remains valid as long as subscription is active and terms are followed * Documenso application code * Self-hosted deployments * Modifications and derivative works * Internal and external distribution (per agreement terms) * Third-party dependencies (these retain their original licenses) * Documenso Cloud service (separate subscription) * Professional services (available separately) ## Pricing Model [#pricing-model] Enterprise pricing is based on: | Factor | Description | | ------------------- | ----------------------------------- | | **Deployment type** | Self-hosted or Documenso Cloud | | **User count** | Number of users sending documents | | **Volume** | Monthly document signing volume | | **Support level** | Standard or premium support options | | **Contract term** | Annual or multi-year agreements | ### Pricing Tiers [#pricing-tiers] Contact [sales](https://documen.so/enterprise) for current pricing. Enterprise agreements typically include: * Annual subscription billing * Volume discounts for larger deployments * Multi-year discount options * Custom terms for specific requirements Pricing and terms are subject to change. Contact sales for current rates and availability. ## Support Included [#support-included] Enterprise licenses include dedicated support beyond community channels. * Email support with priority response * Target response time within 4-8 business hours * Deployment and configuration assistance * Access to technical account resources Available as add-ons or with higher-tier plans: * Dedicated support contact * Scheduled calls for complex issues * Implementation guidance * Custom SLA agreements * On-call support for critical issues See [Support](/docs/policies/support) for complete support options. ## How to Get Started [#how-to-get-started] {/* prettier-ignore */} ### Evaluate your requirements [#evaluate-your-requirements] Determine if you need the Enterprise Edition: * Review the [license comparison](/docs/policies/licenses) to understand AGPL-3.0 vs commercial license * Identify your use case from the scenarios above * List any enterprise features you require ### Contact sales [#contact-sales] Reach out to discuss your needs: * Email: [sales@documenso.com](https://documen.so/enterprise) * Include: Organisation name, use case, estimated user count, deployment preference (self-hosted or cloud) ### Evaluation [#evaluation] Options for evaluating Documenso before purchase: * Test with the Community Edition (same codebase, AGPL-3.0 license) * Request an evaluation license for Enterprise features * Schedule a demo with the sales team ### Agreement and deployment [#agreement-and-deployment] Once terms are agreed: 1. Sign the Enterprise license agreement 2. Receive license key and access credentials 3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud 4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance ### Ongoing [#ongoing] * Renew subscription annually (or per agreement terms) * Access support channels for assistance * Receive updates and new features per your agreement ## Frequently Asked Questions [#frequently-asked-questions] Yes. Your existing deployment can transition to the Enterprise license. Contact [sales](https://documen.so/enterprise) to discuss migration. The license and cloud service are separate. Enterprise cloud plans include both the commercial license and hosted service. Self-hosted Enterprise licenses cover only the license rights. If you don't renew, you lose access to support and updates. Depending on your agreement terms, you may continue using the last licensed version or need to transition to the Community Edition (with AGPL-3.0 compliance). No. Enterprise-only features require an active Enterprise license. The Community Edition includes core document signing functionality. Contact [sales](https://documen.so/enterprise) to discuss evaluation options for your organisation. ## Related [#related] * [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation * [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license * [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ * [Support](/docs/policies/support) - Support channels and response times * [Self-Hosting](/docs/self-hosting) - Deployment guides for self-hosted installations # Fair Use Policy We like to overdeliver, but we cannot overcommit. Our plans are designed to be generous and flexible without forcing customers into rigid volume limits they may never use. Estimating usage at scale is hard, especially over short periods. This fair use policy exists to keep plans sustainable while allowing us to add more value wherever possible without overformalising restrictions. We offer our plans without limits on signing or API volume because we want users and customers to make the most of their accounts. If you are on one of these plans, we ask you to abide by this fair use policy. ## Spirit of the Plan [#spirit-of-the-plan] Use the limitless plans as much as you like. They are meant to offer a lot. Please respect the spirit and intended scope of the account. **What happens if I go beyond the scope of this policy?** We will ask you to upgrade to a fitting plan or custom pricing. We will not block your account without reaching out. You can message us with any questions. ### Do [#do] * Use team or platform plans to run your workflows, even with significant volume, as long as it aligns with the plan’s intended purpose. * Experiment and automate freely within the plan features. * If volume grows beyond what’s sustainable on your plan, we’ll reach out to discuss an upgrade. * Assume that extreme usage will lead to us contacting you. You can scale up—or scale back. It’s about finding the right fit. ### Don't [#dont] * Use an individual account's API to power a platform or product. * Run a large company signing thousands of documents per day on a small team plan. * Expect enterprise-level support on a fair support plan (i.e. business edition). * Use a team plan to power an external platform or commercial product or platform beyond moderate testing. * Expect a platform plan to support enterprise-level volumes indefinitely without a conversation. * Don’t expect the platform plan to cover enterprise-scale volume or support. If you reach that point, we’ll reach out to guide you to the right fit. * Don’t overthink this – if you’re building something valuable, we want to see you succeed. If we need to talk, we will. ## Rate Limits [#rate-limits] API and application requests are rate-limited to protect service availability. Limits apply per user or API token. When a limit is reached, requests return a `429 Too Many Requests` response with a `Retry-After` header indicating when to retry. | Action | Limit | Window | | ------------------------ | ------------- | -------- | | API requests (v1 and v2) | 1000 requests | 1 minute | | File uploads | 20 requests | 1 minute | | AI features | 3 requests | 1 minute | Authentication endpoints (login, signup, password reset, etc.) are also rate-limited to protect against abuse. The API request limit above is the global per-IP ceiling. Individual organisations also have their own rate limits, which may be configured below this value. Rate limits may vary by plan. Enterprise plans can include higher or custom limits. Contact [sales](https://documen.so/sales) for details. Self-hosted deployments can configure their own rate limits. You control all limits based on your infrastructure capacity. See [Self-Hosting](/docs/self-hosting) for deployment options. ## Fair Support [#fair-support] Fair support includes reasonable application-level help for self-hosted users. We will help you get unstuck and point you in the right direction when issues come up. Support is provided in good faith and within reasonable time and effort limits. We are not your operations team and cannot take responsibility for running, monitoring, or maintaining your infrastructure. If you are unsure whether something falls within fair use or fair support, reach out. We are happy to talk it through. ## Related [#related] * [Enterprise Edition](/docs/policies/enterprise-edition) - Commercial licensing and custom limits * [Self-Hosting](/docs/self-hosting) - Deploy without cloud-imposed limits # Policies ## Full Legal Documents [#full-legal-documents] The complete legal documents are available on the main Documenso website: * [Privacy Policy](https://documenso.com/privacy) * [Terms of Service](https://documenso.com/terms) The pages in this documentation provide summaries and practical guidance. For binding legal terms, refer to the full documents linked above. *** ## Related [#related] * [Compliance](/docs/compliance) - Electronic signature standards and regulations # Licenses ## Dual Licensing Overview [#dual-licensing-overview] | Aspect | Community Edition | Enterprise Edition | | ----------------------------- | --------------------------------------------------------- | ------------------------------------------------------------- | | **License** | AGPL-3.0 | Commercial | | **Cost** | Free | Paid | | **Source code disclosure** | Required for network use | Not required | | **Proprietary modifications** | Not permitted | Permitted | | **Support** | Community | Dedicated | | **Use case** | Open-source projects, internal tools with AGPL compliance | Commercial products, SaaS offerings, proprietary integrations | ## Community Edition (AGPL-3.0) [#community-edition-agpl-30] The Community Edition is licensed under the GNU Affero General Public License version 3 (AGPL-3.0). This is a copyleft license that preserves software freedom. ### What AGPL-3.0 Allows [#what-agpl-30-allows] * Use Documenso for any purpose, including commercial use * Access, study, and modify the source code * Distribute copies of the original or modified software * Self-host Documenso on your own infrastructure ### What AGPL-3.0 Requires [#what-agpl-30-requires] The AGPL-3.0 has specific requirements that apply when you modify or distribute Documenso: **Source Code Disclosure**: If you modify Documenso and make it available over a network, you must provide access to the complete source code of your modified version under AGPL-3.0. **Attribution**: You must keep all copyright notices intact and include a copy of the AGPL-3.0 license with any distribution. **Same License**: Derivative works must be licensed under AGPL-3.0. The "network clause" is the distinguishing feature of AGPL-3.0. Unlike GPL-3.0, AGPL-3.0 treats network access as distribution. If users interact with your modified Documenso instance over a network, you must provide them access to the source code. ### When to Use Community Edition [#when-to-use-community-edition] The Community Edition is appropriate when you: * Are building an open-source project * Can comply with AGPL-3.0 requirements * Use Documenso internally without modifications * Want to contribute to the open-source ecosystem * Do not need to keep modifications proprietary See [Community Edition](/docs/policies/community-edition) for complete details. ## Enterprise Edition (Commercial License) [#enterprise-edition-commercial-license] The Enterprise Edition provides a commercial license that removes the AGPL-3.0 requirements. This allows organisations to use, modify, and integrate Documenso without source code disclosure obligations. ### What the Enterprise License Provides [#what-the-enterprise-license-provides] **No Source Code Disclosure**: Keep your modifications private. You are not required to share changes with users or the public. **Proprietary Integration**: Integrate Documenso into proprietary products and services without licensing conflicts. **Commercial Support**: Access to dedicated support channels and SLAs. **Additional Features**: Enterprise-specific features not available in the Community Edition. ### When to Use Enterprise Edition [#when-to-use-enterprise-edition] The Enterprise Edition is appropriate when you: * Build commercial products that incorporate Documenso * Offer document signing as part of a SaaS platform * Cannot or prefer not to open-source your modifications * Need to comply with corporate policies that prohibit AGPL software * Require dedicated support and SLAs If you modify Documenso and provide network access to users (internal or external) without complying with AGPL-3.0 source disclosure requirements, you need an Enterprise license. See [Enterprise Edition](/docs/policies/enterprise-edition) for complete details and pricing. ## Choosing the Right License [#choosing-the-right-license] Use this decision tree to determine which license fits your needs: **No modifications**: Both licenses work. Community Edition is free and sufficient if you run Documenso as-is. **Yes, with modifications**: Continue to the next question. **Yes, under AGPL-3.0**: Community Edition is appropriate. Your modifications must be available to users who access your instance. **No, modifications are proprietary**: Enterprise Edition is required. **Open-source product under AGPL-3.0**: Community Edition is appropriate. **Proprietary product**: Enterprise Edition is required. Using Documenso internally without providing network access to external users does not trigger AGPL-3.0 distribution requirements. However, if your organisation has policies against AGPL software or you make modifications you want to keep private, consider the Enterprise Edition. ## License FAQ [#license-faq] Yes. AGPL-3.0 permits commercial use. You must comply with the license terms, including source code disclosure if you modify and distribute or provide network access. No. Calling the Documenso API from your application does not create a derivative work. Your application remains under your chosen license. However, if you modify Documenso itself and provide network access, AGPL-3.0 requirements apply. Modifying the source code, forking the repository and making changes, or linking Documenso libraries into your application in a way that creates a combined work. Configuration changes and using Documenso through its API are not modifications. You can change branding for your deployment under both licenses. Under AGPL-3.0, you must retain copyright notices and license information in the source code. Under the Enterprise license, you have more flexibility with branding requirements. No. The license applies to the software, not content created with the software. Documents you sign are your property and are not subject to AGPL-3.0. Your existing deployments can transition to the Enterprise license. Contact [sales](https://documen.so/enterprise) to discuss migration. Contributions to the main Documenso repository are licensed under AGPL-3.0. The Enterprise license applies only to your private use and modifications. Contact [sales](https://documen.so/enterprise) to discuss evaluation options. ## Related [#related] * [Licenses](/docs/policies/licenses) - Detailed AGPL-3.0 license explanation * [Community Edition](/docs/policies/community-edition) - Open-source edition details * [Enterprise Edition](/docs/policies/enterprise-edition) - Commercial license details * [Full AGPL-3.0 License Text](https://www.gnu.org/licenses/agpl-3.0.html) - Complete license terms * [GitHub Repository](https://github.com/documenso/documenso) - Source code and LICENSE file # Privacy Policy Our full Privacy Policy is available at [documenso.com/privacy](https://documenso.com/privacy). # Security ## Security Practices [#security-practices] * **Code review**: All code changes require review before merging - **Dependency management**: Dependencies are regularly updated and monitored for vulnerabilities - **Static analysis**: Automated security scanning is part of the CI/CD pipeline - **Open source**: The codebase is publicly available for security review at [github.com/documenso/documenso](https://github.com/documenso/documenso) * **Role-based access**: Teams support member, manager, and admin roles with distinct permissions - **API token scoping**: API tokens can be scoped to specific teams and set with expiration dates - **Session management**: Users can view and revoke active sessions - **Audit logging**: Document actions are logged with timestamps and IP addresses ## Infrastructure Security [#infrastructure-security] ### Documenso Cloud [#documenso-cloud] The hosted cloud service uses the following security measures: | Layer | Implementation | | -------------- | ------------------------------------------------------ | | **Hosting** | Infrastructure hosted in EU data centers | | **Network** | TLS 1.2+ for all connections | | **Database** | Managed PostgreSQL with automated backups | | **Storage** | Encrypted object storage for documents | | **Monitoring** | 24/7 infrastructure monitoring and alerting | | **Updates** | Regular security patches applied to all infrastructure | ### Self-Hosted [#self-hosted] Self-hosted deployments are responsible for their own infrastructure security. See [Self-Hosted Security Considerations](#self-hosted-security-considerations) below. ## Data Encryption [#data-encryption] All data transmitted to and from Documenso is encrypted using TLS 1.2 or higher. This includes: * Web application traffic * API requests * Email delivery (when supported by the receiving server) * Webhook payloads For Documenso Cloud: * Database contents are encrypted at rest * Document storage uses encrypted object storage * Backups are encrypted For self-hosted deployments, encryption at rest depends on your infrastructure configuration. Documenso encrypts sensitive data at the application level: * Encryption keys are configured via `NEXT_PRIVATE_ENCRYPTION_KEY` and `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` * Session tokens are signed using `NEXTAUTH_SECRET` * Passwords are hashed using bcrypt ## Authentication Security [#authentication-security] ### Supported Authentication Methods [#supported-authentication-methods] | Method | Description | | ----------------------------- | ------------------------------------------------ | | **Email and password** | Traditional authentication with hashed passwords | | **OAuth providers** | Google and Microsoft authentication | | **Generic OIDC** | Any OpenID Connect provider | | **Team SSO** | SAML-based single sign-on for enterprise teams | | **Two-factor authentication** | TOTP-based 2FA with recovery codes | | **Passkeys** | WebAuthn-based passwordless authentication | ### Password Requirements [#password-requirements] * Minimum length enforced * Passwords are hashed using bcrypt before storage * Password reset tokens are time-limited and single-use ### Session Security [#session-security] * Sessions can be viewed and revoked from account settings * Session tokens are rotated on authentication events * Idle sessions expire after a configurable period ## Vulnerability Disclosure [#vulnerability-disclosure] Documenso operates a responsible disclosure process for security vulnerabilities. If you discover a security vulnerability, please report it by emailing: **[security@documenso.com](mailto:security@documenso.com)** Include the following information: * Description of the vulnerability * Steps to reproduce * Potential impact * Any suggested fixes (optional) | Stage | Timeline | | --------------------- | ------------------- | | **Acknowledgment** | Within 48 hours | | **Initial triage** | Within 5 days | | **Status update** | Within 10 days | | **Resolution target** | Depends on severity | The following are in scope for vulnerability reports: * Documenso application code * Authentication and authorization flaws * Data exposure vulnerabilities * Injection vulnerabilities * Cross-site scripting (XSS) * Cross-site request forgery (CSRF) The following are out of scope: * Social engineering attacks * Denial of service attacks * Issues in third-party dependencies (report to the upstream project) * Issues requiring physical access to a user's device * Theoretical vulnerabilities without proof of concept We acknowledge security researchers who responsibly disclose vulnerabilities. With your permission, we will credit you when the fix is released. Do not publicly disclose vulnerabilities until they have been addressed. Public disclosure of unpatched vulnerabilities puts users at risk. ## Security Updates [#security-updates] ### Notification [#notification] Security updates are announced through: * [GitHub releases](https://github.com/documenso/documenso/releases) * [GitHub security advisories](https://github.com/documenso/documenso/security/advisories) ### Update Policy [#update-policy] * Critical vulnerabilities are patched as quickly as possible * Security patches are backported to supported versions when feasible * Release notes include security-related changes ### Staying Updated [#staying-updated] For self-hosted deployments: 1. Watch the [GitHub repository](https://github.com/documenso/documenso) for releases 2. Subscribe to security advisories 3. Apply updates promptly, especially security patches See [Upgrades](/docs/self-hosting/maintenance/upgrades) for update procedures. ## Self-Hosted Security Considerations [#self-hosted-security-considerations] When self-hosting Documenso, you are responsible for the security of your deployment. The following recommendations apply: * **Use HTTPS**: Configure TLS certificates for all traffic - **Firewall rules**: Restrict access to necessary ports only - **Network isolation**: Place the database on a private network * **Regular updates**: Keep the host OS and dependencies updated * **Strong secrets**: Generate cryptographically random values for: - `NEXTAUTH_SECRET` - `NEXT_PRIVATE_ENCRYPTION_KEY` - `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` - **Database security**: Use strong passwords and restrict database access - **Environment variables**: Store secrets securely (do not commit to version control) * **Backups**: Implement regular encrypted backups - **Monitoring**: Set up logging and alerting for security events - **Access control**: Limit who has access to the server and database - **Incident response**: Have a plan for responding to security incidents * Store signing certificates securely - Use hardware security modules (HSM) for high-security requirements - Rotate certificates according to your security policy See [Environment Variables](/docs/self-hosting/configuration/environment) for security-related configuration options. Self-hosted deployments have full control over security but also full responsibility. Consider your organisation's security requirements and compliance obligations when configuring your deployment. ## Contact [#contact] For security-related inquiries: * **Security vulnerabilities**: [security@documenso.com](mailto:security@documenso.com) * **General questions**: [support@documenso.com](mailto:support@documenso.com) * **GitHub**: [github.com/documenso/documenso](https://github.com/documenso/documenso) ## Related [#related] * [Security Settings](/docs/users/settings/security) - Account security configuration * [Privacy Policy](/docs/policies/privacy) - Data handling practices * [GDPR](/docs/compliance/gdpr) - Data protection compliance * [Self-Hosting](/docs/self-hosting) - Deploy on your own infrastructure * [Environment Variables](/docs/self-hosting/configuration/environment) - Configuration reference # Support ## Support Channels [#support-channels] | Channel | Availability | Best For | | --------------------------- | ------------ | --------------------------------------- | | Community (Discord, GitHub) | All users | General questions, community discussion | | Email Support | Paid plans | Account issues, billing questions | | Priority Support | Enterprise | Urgent issues, dedicated assistance | ## Community Support [#community-support] Community support is available to all users, including those on free plans and self-hosted deployments. Join the Documenso Discord server to: * Ask questions and get help from the community * Share feedback and feature requests * Connect with other Documenso users * Stay updated on announcements Discord is best for general questions and discussions. Response times vary based on community availability. Use GitHub for technical discussions and bug reports: * **[GitHub Discussions](https://github.com/documenso/documenso/discussions)**: Ask questions, share ideas, and discuss features * **[GitHub Issues](https://github.com/documenso/documenso/issues)**: Report confirmed bugs with reproducible steps When opening an issue, include: 1. Documenso version 2. Deployment method (Cloud, self-hosted Docker, Kubernetes, etc.) 3. Steps to reproduce the problem 4. Expected vs actual behavior 5. Relevant logs (with sensitive information redacted) Do not report security vulnerabilities through public GitHub issues. See the [Security Issues](#security-issues) section below. ## Paid Support Tiers [#paid-support-tiers] Paid plans include access to direct support from the Documenso team. Available on paid subscription plans: * Email support at [support@documenso.com](mailto:support@documenso.com) * Account and billing assistance * Technical guidance for Documenso Cloud usage * Response during business hours Available on [Platform plans](https://documen.so/platform-cta-pricing) and above: * All standard support channels * Private Discord channel for direct communication with the Documenso team * Private Slack workspace for closer collaboration Available with Enterprise subscriptions: * All Platform support channels * Priority response times * Dedicated support contact * Assistance with deployment and integration * Custom configuration guidance * Scheduled calls for complex issues Contact [sales](https://documen.so/enterprise) for Enterprise support options. If you prefer Discord or Slack, contact support to be invited to a private channel or workspace. ## Self-Hosted Support [#self-hosted-support] Self-hosted deployments have access to community support through Discord and GitHub. The following resources are available: * [Self-Hosting Documentation](/docs/self-hosting) - Setup and configuration guides * [Troubleshooting Guide](/docs/self-hosting/maintenance/troubleshooting) - Common issues and solutions * [GitHub Discussions](https://github.com/documenso/documenso/discussions) - Community Q\&A Enterprise licenses for self-hosted deployments can include dedicated support. Contact [sales](https://documen.so/enterprise) for details. ## Response Times [#response-times] Response times depend on the support channel and plan: | Plan | Channel | Target Response Time | | ------------------ | --------------------------- | ------------------------- | | Free / Self-Hosted | Community (Discord, GitHub) | Best effort | | Paid Plans | Email | 1-2 business days | | Enterprise | Priority Support | Within 4-8 business hours | Response times are targets, not guarantees. Actual times may vary based on issue complexity and volume. ## What's Included [#whats-included] * Guidance on Documenso features and functionality * Help with configuration and settings * Troubleshooting application errors * Account and billing questions (paid plans) * Bug reports and issue investigation * Upgrade assistance * Custom development or code modifications * Third-party integration development * Infrastructure setup (servers, databases, networking) * Training sessions (available separately) * Support for modified or forked versions Professional services for custom development, training, and dedicated implementation support are available separately. Contact [sales](https://documen.so/sales) for details. ## Escalation Process [#escalation-process] If your issue is not resolved through initial support channels: {/* prettier-ignore */} ### Community support [#community-support-1] If you don't receive a response on Discord or GitHub within a reasonable time, try the other channel or provide additional details. ### Email support [#email-support] Reply to your existing support thread with additional information. Avoid opening duplicate tickets. ### Request escalation [#request-escalation] For urgent issues on paid plans, request escalation in your support email. ### Enterprise customers [#enterprise-customers] Contact your dedicated support representative directly. For billing disputes or account issues, email [support@documenso.com](mailto:support@documenso.com) with your account details. ## Security Issues [#security-issues] Do not report security vulnerabilities through public channels (GitHub Issues, Discord, etc.). {/* prettier-ignore */} ### Email the security team [#email-the-security-team] Email [security@documenso.com](mailto:security@documenso.com). ### Include details [#include-details] Include a detailed description of the vulnerability. ### Provide steps to reproduce [#provide-steps-to-reproduce] Provide steps to reproduce if possible. ### Allow time for investigation [#allow-time-for-investigation] Allow time for investigation before public disclosure. Documenso follows responsible disclosure practices. Security researchers who report valid vulnerabilities responsibly will be acknowledged. ## Contact [#contact] | Purpose | Contact | | ---------------- | -------------------------------------------------------------- | | General support | [support@documenso.com](mailto:support@documenso.com) | | Security issues | [security@documenso.com](mailto:security@documenso.com) | | Enterprise sales | [sales@documenso.com](https://documen.so/enterprise) | | Privacy requests | [privacy@documenso.com](mailto:privacy@documenso.com) | | Community | [Discord](https://documen.so/discord) | | Bug reports | [GitHub Issues](https://github.com/documenso/documenso/issues) | ## Related [#related] * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Self-hosted issue resolution * [Self-Hosting](/docs/self-hosting) - Deployment and configuration guides * [Enterprise Edition](/docs/policies/enterprise-edition) - Commercial licensing options * [Privacy Policy](/docs/policies/privacy) - How Documenso handles your data # Terms of Service Our full Terms of Service are available at [documenso.com/terms](https://documenso.com/terms). # Verifying Emails from Documenso ## Check the Sender Domain [#check-the-sender-domain] All email sent by Documenso originates from one of the following domains. If you receive an email claiming to be from Documenso and the sender address does not end in one of these domains, treat it as suspicious. | Domain | Used for | | -------------------- | ----------------------------------------------------------------------------------------------- | | `app.documenso.com` | Transactional email | | `documensomail.com` | Transactional email | | `documensoemail.com` | Transactional email | | Custom domain | [Enterprise organisations](/docs/users/organisations/email-domains) using a custom email domain | Typical sender addresses include: * `noreply@app.documenso.com` * `noreply@free.documensomail.com` * `noreply@send.documensoemail.com` A misspelling such as `documenso-email.com`, `documensoemaiI.com` (capital i instead of l), or any other variation is not a Documenso domain. ## Types of Email Documenso Sends [#types-of-email-documenso-sends] Documenso sends email only for the following purposes: * **Account verification** — confirming your email address when you sign up or change it * **Password reset** — a link to reset your password that you requested * **Document invitations** — notifying you that a document has been shared with you to sign, approve, or view * **Signing reminders** — follow-up reminders for pending document actions * **Completed document notifications** — confirmation that all parties have signed a document * **Team invitations** — inviting you to join an organisation or team ## What Documenso Will Never Do [#what-documenso-will-never-do] * Ask for your password via email * Send you an attachment and ask you to open it to verify your identity * Ask you to confirm payment details or billing information over email * Send unsolicited marketing emails if you have not opted in ## How to Tell If an Email Is Legitimate [#how-to-tell-if-an-email-is-legitimate] 1. **Check the sender address** — the domain must be `documenso.com` or `documensomail.com` 2. **Look at the link destination** — hover over any link before clicking; it should point to `app.documenso.com` 3. **Watch for urgency or threats** — legitimate Documenso emails do not threaten account suspension to pressure you into clicking a link immediately 4. **Verify the action yourself** — if in doubt, log in to [app.documenso.com](https://app.documenso.com) directly (not via the email link) and check whether the document or notification exists there ## Report a Suspicious Email [#report-a-suspicious-email] If you receive an email that appears to impersonate Documenso: 1. Do not click any links or download any attachments 2. Forward the email as an attachment to **[support@documenso.com](mailto:support@documenso.com)** 3. Delete the email from your inbox You can also report phishing emails directly to your email provider using their built-in reporting tools. ## Related [#related] * [Security Policy](/docs/policies/security) — Documenso's security practices and vulnerability disclosure process * [Create an Account](/docs/users/getting-started/create-account) — What to expect during sign-up * [Security Settings](/docs/users/settings/security) — Enable two-factor authentication and manage sessions # Self-Hosting ## Getting Started [#getting-started] **You must generate a signing certificate.** Documenso does not ship with one. Without a certificate, the application starts normally but document signing will fail. Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding. **You are responsible for your own network security.** Documenso applies best-effort, non-exhaustive checks to outbound requests such as webhooks, but these are not a complete SSRF mitigation and they fail open. A self-hosted instance can reach internal addresses on your network. Restricting outbound traffic, egress filtering, and blocking access to internal services and cloud metadata endpoints is your responsibility through your firewall and network configuration. *** ## Deployment Options [#deployment-options] Choose a deployment method based on your needs: *** ## Configuration [#configuration] *** ## Maintenance [#maintenance] *** ## Quick Start [#quick-start] Get Documenso running locally in under 5 minutes: ```bash git clone https://github.com/documenso/documenso.git cd documenso cp .env.example .env docker compose -f docker/development/compose.yml up -d ``` Open [http://localhost:3000](http://localhost:3000) to access your instance. See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for detailed instructions and next steps. *** ## Enterprise Edition [#enterprise-edition] Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key. See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license). *** ## Looking for Something Else? [#looking-for-something-else] # User Guide ## Getting Started [#getting-started] New to Documenso? Start here to create your account and send your first document. *** ## Documents [#documents] Upload, prepare, and send documents for electronic signatures. *** ## Templates [#templates] Create reusable templates for documents you send frequently. *** ## Organisations [#organisations] Collaborate with your organisation on document signing. *** ## Settings [#settings] Configure your account and security options. *** ## Prerequisites [#prerequisites] To follow this guide, you need a Documenso account. You can [create a free account](https://app.documenso.com/signup) or use a [self-hosted instance](/docs/self-hosting). *** ## Looking for Something Else? [#looking-for-something-else] # Common Errors This guide provides a comprehensive troubleshooting matrix for the standard error codes returned by the Documenso API. Use this reference to diagnose and resolve integration issues related to envelopes, recipients, and webhooks. ## Application Error Codes [#application-error-codes] | Error Code | Description | Recommended Action | | :------------------------ | :-------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ALREADY_EXISTS` | The resource you are attempting to create already exists. | Verify if the entity (e.g., user, envelope, webhook) has already been instantiated. Use a `PUT` or `PATCH` request to update the existing resource instead of `POST`. | | `EXPIRED_CODE` | The provided access code or token has expired. | Generate a new access code or request a new invitation link before retrying the request. | | `INVALID_BODY` | The request payload is malformed. | Inspect your JSON payload structure. Ensure it strictly adheres to the expected schema and that no required fields are missing. | | `INVALID_REQUEST` | The overall request is malformed or invalid. | Review your API call parameters, including the URL, query parameters, and headers. Correct the request syntax. | | `RECIPIENT_EXPIRED` | The signing link or recipient access has expired. | Generate and resend a new invitation to the affected recipient. | | `LIMIT_EXCEEDED` | Your account usage quota has been exceeded. | Check your current plan limits. Upgrade your subscription or wait until your billing cycle renews. | | `NOT_FOUND` | The requested resource could not be found (404). | Verify the resource ID (envelope, document, webhook) passed in the URL. Ensure the resource has not been deleted. | | `NOT_IMPLEMENTED` | The requested feature is not currently supported by the server. | Consult the API documentation to verify available methods. Do not use this endpoint at this time. | | `NOT_SETUP` | The required configuration for this action is incomplete. | Access your account or integration settings and complete the necessary configuration before retrying. | | `INVALID_CAPTCHA` | Security token (Captcha) validation failed. | Ensure the Captcha token is correctly generated on the client side and transmitted without alteration in your request. | | `UNAUTHORIZED` | Missing or invalid authentication (401). | Verify that your API key is correct, active, and properly formatted in the `Authorization` header (e.g., `Bearer `). | | `FORBIDDEN` | Access to the resource is denied (403). | Ensure your API key or user account has the necessary permissions and roles to execute this specific action. | | `UNKNOWN_ERROR` | An unexpected internal server error occurred (500). | Retry the request later. If the issue persists, contact technical support with your request payload and the timestamp of the incident. | | `RETRY_EXCEPTION` | The operation failed temporarily but can be retried. | Implement an automatic retry logic in your integration, ideally using an exponential backoff strategy. | | `SCHEMA_FAILED` | Strict data schema validation failed. | Verify that the data types sent (string, number, boolean) exactly match the OpenAPI specification. | | `TOO_MANY_REQUESTS` | Rate limit exceeded (429). | Reduce the frequency of your API calls. Implement rate-limiting handling based on the response headers. | | `TWO_FACTOR_AUTH_FAILED` | Two-factor authentication (2FA) failed. | Verify the provided 2FA code. Ensure it was entered correctly and has not expired. | | `WEBHOOK_INVALID_REQUEST` | The webhook-related request is invalid. | Check your receiving endpoint configuration. Ensure the URL is correct and that your server accepts `POST` requests from Documenso. | ## Envelope State Errors [#envelope-state-errors] The following errors occur when attempting to perform actions on an envelope that are incompatible with its current state. | Error Code | Description | Recommended Action | | :------------------- | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | `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_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. | ## See Also [#see-also] * [Documents API](/docs/developers/api/documents) * [Webhooks](/docs/developers/webhooks) # Developer Mode ## Overview [#overview] Developer mode provides additional tools and features to help you integrate and debug Documenso. ## Field Information [#field-information] When enabled, developer mode displays the following information for each field: * **Field ID** - The unique identifier of the field * **Recipient ID** - The ID of the recipient assigned to the field * **Pos X / Pos Y** - The position of the field on the page * **Width / Height** - The dimensions of the field To enable developer mode, add the `devmode=true` query parameter to the editor URL. ```bash # Legacy editor https://app.documenso.com/t//documents//legacy_editor?devmode=true ``` Field Coordinates Legacy Editor ```bash # New editor https://app.documenso.com/t//documents//edit?step=addFields&devmode=true ``` Field Coordinates New Editor *** ## See Also [#see-also] * [Fields API](/docs/developers/api/fields) - Create and position fields via API * [Field Types](/docs/concepts/field-types) - Detailed field type reference # Documents API This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). ## Overview [#overview] [Documents](/docs/users/documents) (called "envelopes" in the API) are the core resource in Documenso. You can: 1. create documents with recipients and fields 2. send them for signing 3. track their status 4. retrieve the completed PDFs Each document contains one or more PDF files, a list of recipients, and the fields they need to fill. ## Document Object [#document-object] A document object contains the following properties: | Property | Type | Description | | ------------------- | -------------- | ----------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier (e.g., `envelope_abc123`) | | `secondaryId` | string | Legacy identifier in prefixed form (`document_123` for documents, `template_123` for templates) | | `internalVersion` | number | Internal envelope schema version | | `type` | string | `DOCUMENT` or `TEMPLATE` | | `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, or `CANCELLED` | | `title` | string | Document title | | `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | | `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` | | `templateType` | string | Template visibility: `PUBLIC`, `PRIVATE`, or `ORGANISATION` (only meaningful for templates) | | `externalId` | string \| null | Your custom identifier for the document | | `userId` | number | ID of the user who owns the document | | `teamId` | number | ID of the team the document belongs to | | `folderId` | string \| null | ID of the folder containing the document | | `templateId` | number \| null | Legacy ID of the template this document was created from | | `authOptions` | object \| null | Access and action authentication requirements | | `formValues` | object \| null | Pre-filled form values | | `publicTitle` | string | Public title shown on profile and direct-link pages | | `publicDescription` | string | Public description shown on profile and direct-link pages | | `createdAt` | string | ISO 8601 timestamp | | `updatedAt` | string | ISO 8601 timestamp | | `completedAt` | string \| null | Timestamp when all recipients completed signing | | `deletedAt` | string \| null | Timestamp if soft-deleted | | `recipients` | array | List of recipients and their signing status | | `fields` | array | Signature and form fields on the document | | `envelopeItems` | array | PDF files attached to the document | | `directLink` | object \| null | Direct-link signing configuration (`id`, `token`, `enabled`, `directTemplateRecipientId`) | | `team` | object | Owning team (`id`, `url`) | | `user` | object | Document owner (`id`, `name`, `email`) | | `documentMeta` | object | Email settings, redirect URL, signing options | Documents created through the API have `source: "DOCUMENT"` — there is no separate `API` source value. To tag documents created by your integration, set `externalId` when creating them. ### Example Document Object [#example-document-object] ```json { "id": "envelope_abc123xyz", "secondaryId": "document_123", "internalVersion": 2, "type": "DOCUMENT", "status": "PENDING", "source": "DOCUMENT", "visibility": "EVERYONE", "templateType": "PRIVATE", "title": "Service Agreement", "externalId": "contract-2025-001", "userId": 1, "teamId": 1, "folderId": null, "templateId": null, "authOptions": null, "formValues": null, "publicTitle": "", "publicDescription": "", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:35:00.000Z", "completedAt": null, "deletedAt": null, "recipients": [ { "id": 1, "email": "signer@example.com", "name": "John Smith", "role": "SIGNER", "signingStatus": "NOT_SIGNED", "signingOrder": 1 } ], "fields": [ { "id": 123, "secondaryId": "field_abc123", "type": "SIGNATURE", "recipientId": 1, "envelopeId": "envelope_abc123xyz", "envelopeItemId": "envelope_item_xyz", "page": 1, "positionX": "10", "positionY": "80", "width": "30", "height": "5", "customText": "", "inserted": false, "fieldMeta": null } ], "envelopeItems": [ { "id": "envelope_item_xyz", "envelopeId": "envelope_abc123xyz", "documentDataId": "doc_data_abc123", "title": "contract.pdf", "order": 1 } ], "directLink": null, "team": { "id": 1, "url": "your-team" }, "user": { "id": 1, "name": "Jane Smith", "email": "jane@example.com" }, "documentMeta": { "subject": "Please sign this document", "message": "Hi, please review and sign this agreement.", "timezone": "America/New_York", "redirectUrl": "https://example.com/thank-you" } } ``` Field position and size values are stored as decimals and serialized as strings in API responses. ## List Documents [#list-documents] Retrieve a paginated list of documents. ``` GET /envelope ``` ### Query Parameters [#query-parameters] | Parameter | Type | Description | | ------------------ | ------- | -------------------------------------------------------------------------- | | `page` | integer | Page number (default: 1) | | `perPage` | integer | Results per page (default: 10, max: 100) | | `type` | string | Filter by `DOCUMENT` or `TEMPLATE` | | `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | | `source` | string | Filter by creation source | | `folderId` | string | Filter by folder ID | | `orderByColumn` | string | Sort field (only `createdAt` supported) | | `orderByDirection` | string | Sort direction: `asc` or `desc` (default: `desc`) | ### Code Examples [#code-examples] ```bash # List all documents curl -X GET "https://app.documenso.com/api/v2/envelope" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" # Filter by status and paginate curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING&page=1&perPage=20" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" # List only documents (not templates) curl -X GET "https://app.documenso.com/api/v2/envelope?type=DOCUMENT" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; // List all documents const response = await fetch(`${BASE_URL}/envelope`, { method: 'GET', headers: { Authorization: API_TOKEN, }, }); const { data, count } = await response.json(); console.log(`Found ${count} documents`); // Filter by status const pendingResponse = await fetch( `${BASE_URL}/envelope?status=PENDING&page=1&perPage=20`, { method: 'GET', headers: { Authorization: API_TOKEN, }, } ); const pendingDocs = await pendingResponse.json(); ``` ### Response [#response] ```json { "data": [ { "id": "envelope_abc123", "type": "DOCUMENT", "status": "PENDING", "title": "Service Agreement", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:35:00.000Z", "recipients": [ { "id": 1, "email": "signer@example.com", "name": "John Smith", "role": "SIGNER", "signingStatus": "NOT_SIGNED" } ] } ], "count": 42, "currentPage": 1, "perPage": 10, "totalPages": 5 } ``` *** ## Get Document [#get-document] Retrieve a single document by ID. ``` GET /envelope/{envelopeId} ``` ### Path Parameters [#path-parameters] | Parameter | Type | Description | | ------------ | ------ | ----------------------------------------- | | `envelopeId` | string | The document ID (e.g., `envelope_abc123`) | ### Code Examples [#code-examples-1] ```bash curl -X GET "https://app.documenso.com/api/v2/envelope/envelope_abc123" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const envelopeId = 'envelope_abc123'; const response = await fetch(`https://app.documenso.com/api/v2/envelope/${envelopeId}`, { method: 'GET', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, }); const document = await response.json(); console.log(document.title, document.status); ``` ### Response [#response-1] Returns the full document object including recipients, fields, and envelope items. ```json { "id": "envelope_abc123", "type": "DOCUMENT", "status": "PENDING", "title": "Service Agreement", "recipients": [...], "fields": [...], "envelopeItems": [...], "documentMeta": {...} } ``` *** ## Create Document [#create-document] Create a new document with optional recipients and fields in a single request. This endpoint automatically scans uploaded PDFs for [placeholder patterns](/docs/users/documents/advanced/pdf-placeholders) like `{"{{signature, r1}}"}` and creates fields at those locations. ``` POST /envelope/create Content-Type: multipart/form-data ``` ### Request Body [#request-body] The request uses `multipart/form-data` with two parts: | Part | Type | Description | | --------- | ------- | ---------------------- | | `payload` | JSON | Document configuration | | `files` | File(s) | One or more PDF files | ### Payload Schema [#payload-schema] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------- | | `type` | string | Yes | Must be `DOCUMENT` | | `title` | string | Yes | Document title | | `externalId` | string | No | Your custom identifier | | `visibility` | string | No | `EVERYONE`, `ADMIN`, or `MANAGER_AND_ABOVE` | | `folderId` | string | No | Folder ID to create the document in | | `recipients` | array | No | Recipients with optional fields | | `meta` | object | No | Email subject, message, redirect URL, etc. | ### Code Examples [#code-examples-2] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/create" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: multipart/form-data" \ -F 'payload={ "type": "DOCUMENT", "title": "Service Agreement", "externalId": "contract-2025-001", "recipients": [ { "email": "signer@example.com", "name": "John Smith", "role": "SIGNER", "fields": [ { "identifier": 0, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5 }, { "identifier": 0, "type": "DATE", "page": 1, "positionX": 50, "positionY": 80, "width": 20, "height": 3 } ] } ], "meta": { "subject": "Please sign this agreement", "message": "Hi John, please review and sign the attached agreement.", "redirectUrl": "https://example.com/thank-you" } }' \ -F "files=@./contract.pdf;type=application/pdf" ``` ```typescript import fs from 'fs'; import FormData from 'form-data'; const form = new FormData(); const payload = { type: 'DOCUMENT', title: 'Service Agreement', externalId: 'contract-2025-001', recipients: [ { email: 'signer@example.com', name: 'John Smith', role: 'SIGNER', fields: [ { identifier: 0, type: 'SIGNATURE', page: 1, positionX: 10, positionY: 80, width: 30, height: 5, }, { identifier: 0, type: 'DATE', page: 1, positionX: 50, positionY: 80, width: 20, height: 3, }, ], }, ], meta: { subject: 'Please sign this agreement', message: 'Hi John, please review and sign the attached agreement.', redirectUrl: 'https://example.com/thank-you', }, }; form.append('payload', JSON.stringify(payload)); form.append('files', fs.createReadStream('./contract.pdf'), { contentType: 'application/pdf', }); const response = await fetch('https://app.documenso.com/api/v2/envelope/create', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, body: form, }); const { id } = await response.json(); console.log('Created document:', id); ``` ### Response [#response-2] ```json { "id": "envelope_abc123xyz" } ``` ### Field Positioning [#field-positioning] Field positions use percentage values (0-100) relative to the PDF page: | Parameter | Description | | ------------ | ---------------------------------------------------------- | | `positionX` | Horizontal position from left edge (0 = left, 100 = right) | | `positionY` | Vertical position from top edge (0 = top, 100 = bottom) | | `width` | Field width as percentage of page width | | `height` | Field height as percentage of page height | | `page` | Page number (1-indexed) | | `identifier` | File index (0 for first file) or filename | ### Field Types [#field-types] | Type | Description | | ----------- | --------------------------- | | `SIGNATURE` | Signature field | | `INITIALS` | Initials field | | `NAME` | Auto-filled recipient name | | `EMAIL` | Auto-filled recipient email | | `DATE` | Signing date | | `TEXT` | Free text input | | `NUMBER` | Numeric input | | `CHECKBOX` | Checkbox selection | | `RADIO` | Radio button group | | `DROPDOWN` | Dropdown selection | ### Recipient Roles [#recipient-roles] | Role | Description | | ---------- | ----------------------------------------- | | `SIGNER` | Must sign the document | | `APPROVER` | Must approve before signers can sign | | `CC` | Receives a copy but doesn't sign | | `VIEWER` | Can view the document but takes no action | *** ## Update Document [#update-document] Update a document's properties. Only works on documents in `DRAFT` status. ``` POST /envelope/update ``` ### Request Body [#request-body-1] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------ | | `envelopeId` | string | Yes | Document ID | | `data` | object | No | Document properties to update | | `meta` | object | No | Email and signing settings to update | ### Code Examples [#code-examples-3] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/update" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "envelopeId": "envelope_abc123", "data": { "title": "Updated Service Agreement", "externalId": "contract-2025-001-v2" }, "meta": { "subject": "Updated: Please sign this agreement", "redirectUrl": "https://example.com/signed" } }' ``` ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope/update', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'envelope_abc123', data: { title: 'Updated Service Agreement', externalId: 'contract-2025-001-v2', }, meta: { subject: 'Updated: Please sign this agreement', redirectUrl: 'https://example.com/signed', }, }), }); const document = await response.json(); ``` *** ## Send Document [#send-document] Send a document to recipients for signing. This changes the status from `DRAFT` to `PENDING`. ``` POST /envelope/distribute ``` ### Request Body [#request-body-2] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------- | | `envelopeId` | string | Yes | Document ID | | `meta` | object | No | Override email settings for this send | ### Code Examples [#code-examples-4] ```bash # Basic send curl -X POST "https://app.documenso.com/api/v2/envelope/distribute" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "envelopeId": "envelope_abc123" }' # Send with custom email settings curl -X POST "https://app.documenso.com/api/v2/envelope/distribute" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "envelopeId": "envelope_abc123", "meta": { "subject": "Action Required: Sign Agreement", "message": "Please sign this document by end of day.", "timezone": "America/New_York" } }' ``` ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope/distribute', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'envelope_abc123', meta: { subject: 'Action Required: Sign Agreement', message: 'Please sign this document by end of day.', }, }), }); const { id, recipients } = await response.json(); // Recipients now include signing URLs recipients.forEach((r) => { console.log(`${r.email}: ${r.signingUrl}`); }); ``` ### Response [#response-3] The response includes signing URLs for each recipient: ```json { "success": true, "id": "envelope_abc123", "recipients": [ { "id": 1, "name": "John Smith", "email": "signer@example.com", "token": "abc123xyz", "role": "SIGNER", "signingOrder": 1, "signingUrl": "https://app.documenso.com/sign/abc123xyz" } ] } ``` Use the `signingUrl` to redirect recipients directly to the signing page, or let them use the email link. *** ## Cancel Document [#cancel-document] Cancel a pending document. This changes its status from `PENDING` to `CANCELLED`. ``` POST /envelope/cancel ``` ### Request Body [#request-body-3] | Field | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------- | | `envelopeId` | string | Yes | Document ID | | `reason` | string | No | Reason for cancelling the document | ### Code Examples [#code-examples-5] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/cancel" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "envelopeId": "envelope_abc123", "reason": "The agreement is no longer needed." }' ``` ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope/cancel', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'envelope_abc123', reason: 'The agreement is no longer needed.', }), }); const { success } = await response.json(); ``` ### Response [#response-4] ```json { "success": true } ``` ### Behavior [#behavior] * Only documents in `PENDING` status can be cancelled. Other statuses return `400`. * Cancellation is not idempotent. Cancelling the same document again returns `400`. * The document owner and team members with `MANAGER` or higher permissions can cancel it. Requests for documents you cannot view return `404`; requests for visible documents without sufficient permissions return `401`. * A successful cancellation fires the `DOCUMENT_CANCELLED` webhook. * Cancellation emails are sent only to eligible non-CC, non-rejected recipients who were sent or opened the document. *** ## Delete Document [#delete-document] Delete a document. Completed documents cannot be deleted. ``` POST /envelope/delete ``` ### Request Body [#request-body-4] | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `envelopeId` | string | Yes | Document ID | ### Code Examples [#code-examples-6] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/delete" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "envelopeId": "envelope_abc123" }' ``` ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope/delete', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'envelope_abc123', }), }); const { success } = await response.json(); ``` ### Response [#response-5] ```json { "success": true } ``` *** ## Get Multiple Documents [#get-multiple-documents] Retrieve multiple documents by their IDs in a single request. ``` POST /envelope/get-many ``` ### Request Body [#request-body-5] | Field | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------------------------------- | | `ids` | object | Yes | ID selector containing `type` and `ids` | | `ids.type` | string | Yes | `envelopeId`, `documentId`, or `templateId` | | `ids.ids` | array | Yes | 1-20 IDs: strings for `envelopeId`; numbers for `documentId` or `templateId` | ### Code Examples [#code-examples-7] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/get-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "ids": { "type": "envelopeId", "ids": ["envelope_abc123", "envelope_def456", "envelope_ghi789"] } }' ``` ```typescript const requestedIds = ['envelope_abc123', 'envelope_def456', 'envelope_ghi789']; const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: { type: 'envelopeId', ids: requestedIds, }, }), }); const { data } = await response.json(); ``` ### Response [#response-6] ```json { "data": [ { "id": "envelope_abc123", "type": "DOCUMENT", "status": "PENDING", "title": "Service Agreement" } ] } ``` The endpoint silently omits envelopes you cannot access instead of returning `404`. Compare `data.length` with `requestedIds.length` to detect omissions. *** ## Document Statuses [#document-statuses] | Status | Description | | ----------- | ---------------------------------------------------------------------------------------------- | | `DRAFT` | Document is being prepared. Recipients have not been notified. | | `PENDING` | Document has been sent. Waiting for recipients to sign. | | `COMPLETED` | All recipients have signed. Document is sealed. | | `REJECTED` | A recipient rejected the document. | | `CANCELLED` | The document was cancelled by its owner or a team member with `MANAGER` or higher permissions. | ### Status Transitions [#status-transitions] * **DRAFT to PENDING**: Call the distribute endpoint * **PENDING to COMPLETED**: All recipients complete their signing * **PENDING to REJECTED**: A recipient rejects the document * **PENDING to CANCELLED**: The document owner or a team member with `MANAGER` or higher permissions cancels the document You cannot modify recipients or fields after a document moves to `PENDING` status. *** ## Filtering and Pagination [#filtering-and-pagination] ### Pagination Parameters [#pagination-parameters] | Parameter | Type | Default | Description | | --------- | ------- | ------- | --------------------------- | | `page` | integer | 1 | Page number | | `perPage` | integer | 10 | Results per page (max: 100) | ### Filter Parameters [#filter-parameters] | Parameter | Values | Description | | ---------- | -------------------------------------------------------- | ------------------------- | | `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type | | `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | Filter by status | | `source` | `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | Filter by creation source | | `folderId` | string | Filter by folder | ### Sorting [#sorting] | Parameter | Values | Description | | ------------------ | ------------- | -------------------------------- | | `orderByColumn` | `createdAt` | Field to sort by | | `orderByDirection` | `asc`, `desc` | Sort direction (default: `desc`) | ### Example: Fetch All Pending Documents [#example-fetch-all-pending-documents] ```typescript async function getAllPendingDocuments() { const documents = []; let page = 1; let hasMore = true; while (hasMore) { const response = await fetch( `https://app.documenso.com/api/v2/envelope?status=PENDING&page=${page}&perPage=100`, { headers: { Authorization: 'api_xxxxxxxxxxxxxxxx' }, }, ); const { data, currentPage, totalPages } = await response.json(); documents.push(...data); hasMore = currentPage < totalPages; page++; } return documents; } ``` *** ## See Also [#see-also] * [Recipients API](/docs/developers/api/recipients) - Add and manage document recipients * [Fields API](/docs/developers/api/fields) - Add signature and form fields * [Templates API](/docs/developers/api/templates) - Create reusable document templates * [Webhooks](/docs/developers/webhooks) - Get notified when documents are signed # Fields API This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). ## Field Object [#field-object] | Property | Type | Description | | ---------------- | -------------- | -------------------------------------------- | | `id` | number | Unique field identifier | | `secondaryId` | string | Secondary identifier for audit logs | | `type` | string | Field type (see [Field Types](#field-types)) | | `recipientId` | number | ID of the recipient assigned to this field | | `envelopeId` | number | ID of the parent envelope | | `envelopeItemId` | string | ID of the PDF item the field is placed on | | `page` | number | Page number (1-indexed) | | `positionX` | number | X coordinate as percentage (0-100) | | `positionY` | number | Y coordinate as percentage (0-100) | | `width` | number | Width as percentage of page (0-100) | | `height` | number | Height as percentage of page (0-100) | | `customText` | string | Value entered by the recipient | | `inserted` | boolean | Whether the field has been completed | | `fieldMeta` | object \| null | Type-specific configuration options | ### Example Field Object [#example-field-object] ```json { "id": 456, "secondaryId": "field_abc123", "type": "SIGNATURE", "recipientId": 123, "envelopeId": 789, "envelopeItemId": "envelope_item_xyz", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5, "customText": "", "inserted": false, "fieldMeta": { "type": "signature", "required": true } } ``` *** ## Field Types [#field-types] | Type | Description | Auto-filled | | ---------------- | ----------------------------------------- | ----------- | | `SIGNATURE` | Drawn, typed, or uploaded signature | No | | `FREE_SIGNATURE` | Unrestricted signature without validation | No | | `INITIALS` | Recipient's initials | No | | `NAME` | Recipient's full name | Yes | | `EMAIL` | Recipient's email address | Yes | | `DATE` | Date the field was completed | Yes | | `TEXT` | Free-form text input | No | | `NUMBER` | Numeric input with optional validation | No | | `RADIO` | Single selection from options | No | | `CHECKBOX` | Multiple selections from options | No | | `DROPDOWN` | Single selection from a dropdown menu | No | *** ## Get Field [#get-field] Retrieve a single field by ID. ``` GET /envelope/field/{fieldId} ``` ### Path Parameters [#path-parameters] | Parameter | Type | Description | | --------- | ------ | ------------ | | `fieldId` | number | The field ID | ### Code Examples [#code-examples] ```bash curl -X GET "https://app.documenso.com/api/v2/envelope/field/456" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/field/456', { method: 'GET', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, } ); const field = await response.json(); console.log(field.type, field.page); ``` ### Response [#response] Returns the field object. *** ## Create Fields [#create-fields] Add one or more fields to a document. ``` POST /envelope/field/create-many ``` ### Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | | `documentId` | number | Yes | The document ID | | `fields` | array | Yes | Array of field configurations | ### Code Examples [#code-examples-1] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/field/create-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "documentId": 123, "fields": [ { "type": "SIGNATURE", "recipientId": 456, "pageNumber": 1, "pageX": 10, "pageY": 80, "width": 30, "height": 5 }, { "type": "DATE", "recipientId": 456, "pageNumber": 1, "pageX": 50, "pageY": 80, "width": 20, "height": 3 }, { "type": "TEXT", "recipientId": 456, "pageNumber": 1, "pageX": 10, "pageY": 70, "width": 40, "height": 4, "fieldMeta": { "type": "text", "label": "Job Title", "placeholder": "Enter your job title", "required": true } } ] }' ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/field/create-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ documentId: 123, fields: [ { type: 'SIGNATURE', recipientId: 456, pageNumber: 1, pageX: 10, pageY: 80, width: 30, height: 5, }, { type: 'DATE', recipientId: 456, pageNumber: 1, pageX: 50, pageY: 80, width: 20, height: 3, }, { type: 'TEXT', recipientId: 456, pageNumber: 1, pageX: 10, pageY: 70, width: 40, height: 4, fieldMeta: { type: 'text', label: 'Job Title', placeholder: 'Enter your job title', required: true, }, }, ], }), } ); const { fields } = await response.json(); console.log(`Created ${fields.length} fields`); ``` ### Response [#response-1] ```json { "fields": [ { "id": 101, "type": "SIGNATURE", "recipientId": 456, "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5 }, { "id": 102, "type": "DATE", "recipientId": 456, "page": 1, "positionX": 50, "positionY": 80, "width": 20, "height": 3 }, { "id": 103, "type": "TEXT", "recipientId": 456, "page": 1, "positionX": 10, "positionY": 70, "width": 40, "height": 4 } ] } ``` *** ## Update Fields [#update-fields] Update one or more fields in a single request. ``` POST /envelope/field/update-many ``` ### Request Body [#request-body-1] | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | | `documentId` | number | Yes | The document ID | | `fields` | array | Yes | Array of field update objects | ### Code Examples [#code-examples-2] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/field/update-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "documentId": 123, "fields": [ { "id": 101, "type": "SIGNATURE", "pageY": 85 }, { "id": 102, "type": "DATE", "pageY": 85 } ] }' ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/field/update-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ documentId: 123, fields: [ { id: 101, type: 'SIGNATURE', pageY: 85 }, { id: 102, type: 'DATE', pageY: 85 }, ], }), } ); const { fields } = await response.json(); ``` ### Response [#response-2] ```json { "fields": [ { "id": 101, "type": "SIGNATURE", "positionY": 85 }, { "id": 102, "type": "DATE", "positionY": 85 } ] } ``` *** ## Delete Field [#delete-field] Remove a field from a document. ``` POST /envelope/field/delete ``` ### Request Body [#request-body-2] | Field | Type | Required | Description | | --------- | ------ | -------- | ------------ | | `fieldId` | number | Yes | The field ID | ### Code Examples [#code-examples-3] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/field/delete" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "fieldId": 456 }' ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/field/delete', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ fieldId: 456, }), } ); const { success } = await response.json(); ``` ### Response [#response-3] ```json { "success": true } ``` *** ## Field Positioning [#field-positioning] Fields use percentage-based coordinates relative to the PDF page dimensions. | Property | Range | Description | | ----------- | ----- | -------------------------------------------------- | | `positionX` | 0-100 | Horizontal position from left edge (0 = left edge) | | `positionY` | 0-100 | Vertical position from top edge (0 = top edge) | | `width` | 0-100 | Field width as percentage of page width | | `height` | 0-100 | Field height as percentage of page height | | `page` | 1+ | Page number (1-indexed) | ### Coordinate System [#coordinate-system] ``` (0,0) ─────────────────────────── (100,0) │ │ │ ┌─────────┐ │ │ │ Field │ (pageX: 10, │ │ │ │ pageY: 20, │ │ └─────────┘ width: 30, │ │ height: 5) │ │ │ (0,100) ─────────────────────────(100,100) ``` ### Example: Position a Signature at Bottom Right [#example-position-a-signature-at-bottom-right] ```typescript const field = { type: 'SIGNATURE', recipientId: 123, pageNumber: 1, pageX: 60, // 60% from left pageY: 85, // 85% from top (near bottom) width: 30, // 30% of page width height: 8, // 8% of page height }; ``` *** ## Placeholder-Based Field Positioning [#placeholder-based-field-positioning] Instead of specifying exact coordinates, you can position fields using placeholder text embedded in your PDF. Include placeholder markers such as `{{signature, r1}}` in your document, and Documenso will create fields at those locations when the document is uploaded. This approach is useful when generating PDFs programmatically or using templates with consistent layouts. Placeholder support is only available in `envelope.*` endpoints. `POST /template/use` does not support placeholder parsing. See the [PDF Placeholders](/docs/users/documents/advanced/pdf-placeholders) guide for the full placeholder format reference, including supported field types, recipient identifiers, and field options. *** ## Field Meta Options [#field-meta-options] Each field type supports specific configuration through `fieldMeta`. ### Common Options [#common-options] All field types support these base options: | Option | Type | Description | | ------------- | ------- | --------------------------------------- | | `label` | string | Display text shown near the field | | `placeholder` | string | Hint text when field is empty | | `required` | boolean | Whether field must be completed | | `readOnly` | boolean | Lock field with a pre-filled value | | `fontSize` | number | Text size in pixels (8-96, default: 12) | ### Signature Field [#signature-field] ```json { "type": "SIGNATURE", "fieldMeta": { "type": "signature", "required": true } } ``` ### Text Field [#text-field] ```json { "type": "TEXT", "fieldMeta": { "type": "text", "label": "Company Name", "placeholder": "Enter company name", "text": "Default value", "characterLimit": 100, "textAlign": "left", "required": true } } ``` | Option | Type | Description | | ---------------- | ------ | ---------------------------------- | | `text` | string | Default value | | `characterLimit` | number | Maximum characters allowed | | `textAlign` | string | `left`, `center`, or `right` | | `lineHeight` | number | Spacing between lines (1-10) | | `letterSpacing` | number | Spacing between characters (0-100) | ### Number Field [#number-field] ```json { "type": "NUMBER", "fieldMeta": { "type": "number", "label": "Quantity", "minValue": 1, "maxValue": 100, "value": "10", "required": true } } ``` | Option | Type | Description | | -------------- | ------ | --------------------- | | `value` | string | Default value | | `minValue` | number | Minimum allowed value | | `maxValue` | number | Maximum allowed value | | `numberFormat` | string | Display format | ### Date Field [#date-field] ```json { "type": "DATE", "fieldMeta": { "type": "date", "textAlign": "left", "required": true } } ``` ### Checkbox Field [#checkbox-field] ```json { "type": "CHECKBOX", "fieldMeta": { "type": "checkbox", "label": "Agreements", "values": [ { "id": 1, "value": "Terms of Service", "checked": false }, { "id": 2, "value": "Privacy Policy", "checked": false } ], "validationRule": "min", "validationLength": 1, "direction": "vertical", "required": true } } ``` | Option | Type | Description | | ------------------ | ------ | --------------------------------- | | `values` | array | List of checkbox options | | `validationRule` | string | Validation type for selections | | `validationLength` | number | Number for validation rule | | `direction` | string | `vertical` or `horizontal` layout | ### Radio Field [#radio-field] ```json { "type": "RADIO", "fieldMeta": { "type": "radio", "label": "Payment Method", "values": [ { "id": 1, "value": "Credit Card", "checked": false }, { "id": 2, "value": "Bank Transfer", "checked": true }, { "id": 3, "value": "Check", "checked": false } ], "direction": "vertical", "required": true } } ``` ### Dropdown Field [#dropdown-field] ```json { "type": "DROPDOWN", "fieldMeta": { "type": "dropdown", "label": "Country", "values": [{ "value": "United States" }, { "value": "Canada" }, { "value": "United Kingdom" }], "defaultValue": "United States", "required": true } } ``` | Option | Type | Description | | -------------- | ------ | ------------------------ | | `values` | array | List of dropdown options | | `defaultValue` | string | Pre-selected option | *** ## Complete Example [#complete-example] Create a document with a signature block containing multiple field types: ```typescript async function addSignatureBlock(documentId: number, recipientId: number) { const fields = [ // Signature { type: 'SIGNATURE', recipientId, pageNumber: 1, pageX: 10, pageY: 80, width: 30, height: 8, fieldMeta: { type: 'signature', required: true, }, }, // Printed name { type: 'NAME', recipientId, pageNumber: 1, pageX: 10, pageY: 90, width: 30, height: 4, fieldMeta: { type: 'name', label: 'Printed Name', }, }, // Date { type: 'DATE', recipientId, pageNumber: 1, pageX: 50, pageY: 80, width: 20, height: 4, fieldMeta: { type: 'date', label: 'Date', }, }, // Job title { type: 'TEXT', recipientId, pageNumber: 1, pageX: 50, pageY: 90, width: 30, height: 4, fieldMeta: { type: 'text', label: 'Title', placeholder: 'Enter your job title', }, }, ]; const response = await fetch('https://app.documenso.com/api/v2/envelope/field/create-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ documentId, fields }), }); return response.json(); } ``` *** ## Error Responses [#error-responses] | Status | Description | | ------ | ---------------------------------------------------- | | `400` | Invalid field configuration or document already sent | | `401` | Invalid or missing API key | | `404` | Document, recipient, or field not found | | `500` | Server error | Fields cannot be modified after a document is sent for signing. Make all field changes while the document is in `DRAFT` status. *** ## See Also [#see-also] * [Documents API](/docs/developers/api/documents) - Create and manage documents * [Recipients API](/docs/developers/api/recipients) - Add signers to documents * [Field Types](/docs/concepts/field-types) - Detailed field type reference # API Reference The guides below cover common API patterns but may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). ## Base URL [#base-url] ``` https://app.documenso.com/api/v2 ``` For self-hosted instances, replace with your instance URL. *** ## Authentication [#authentication] All requests require an API key in the `Authorization` header: ``` Authorization: api_xxxxxxxxxxxxxxxx ``` See [Authentication](/docs/developers/getting-started/authentication) for details. *** ## Endpoints [#endpoints] *** ## See Also [#see-also] * [First API Call](/docs/developers/getting-started/first-api-call) - Quick start example * [Webhooks](/docs/developers/webhooks) - Get notified about document events # Migrating to Envelopes ## Summary [#summary] The following items have been deprecated and will be removed on the 1st of March 2027: * API V1 * A subset of SDK/API V2 endpoints * Legacy documents and templates * EmbedCreateDocumentV1 * EmbedCreateTemplateV1 * EmbedUpdateDocumentV1 * EmbedUpdateTemplateV1 The beta endpoint `/api/v2-beta` will also be removed. Use `/api/v2` instead, which is a drop-in replacement. Nothing breaks before 1st of March 2027, so you can migrate at your own pace. ## What are legacy documents and templates [#what-are-legacy-documents-and-templates] These are documents and templates created by the following endpoints: * `POST /api/v2/document/create` * `POST /api/v2/document/create/beta` * `POST /api/v2/template/create` * `POST /api/v2/template/create/beta` * `POST /api/v1/documents` * `POST /api/v1/templates` * `POST /api/v1/templates/create-document` * `POST /api/v1/templates/generate-document` ## What replaces legacy documents and templates [#what-replaces-legacy-documents-and-templates] At the end of 2025 we introduced a unified system for documents and templates, called envelopes. We still reference documents and templates throughout the documentation and application to distinguish them, but internally they are envelopes. Moving to the envelope system gives you: * **Multiple PDFs in one envelope.** Send several documents to sign in a single request. * **One API for documents and templates.** Learn one set of endpoints instead of two misaligned ones. * **A better editor and signing experience** for you and your recipients. ## How to migrate [#how-to-migrate] {/* prettier-ignore */} ### Switch to the envelope endpoints [#switch-to-the-envelope-endpoints] Replace each deprecated endpoint with its `/api/v2/envelope/*` equivalent from the [mapping tables](#endpoint-mapping-reference) below. ### Set the envelope `type` on create [#set-the-envelope-type-on-create] A single endpoint, `POST /api/v2/envelope/create`, can create both documents and templates. Set `type` to `DOCUMENT` or `TEMPLATE`. You can now upload more than one PDF using the `files` field. ### Update how you store IDs [#update-how-you-store-ids] Envelope IDs are **strings** (for example `envelope_abc123`), not numbers. Update any code that stores, parses, or compares IDs. ### Test, then remove the old calls [#test-then-remove-the-old-calls] Verify the new flow against your account, then delete the deprecated calls. The main data differences are as follows: * ID format changed from number to string (e.g. `42` to `envelope_abc123`) * pageNumber becomes page * pageX becomes positionX * pageY becomes positionY See the [Documents API](/docs/developers/api/documents) and [Templates API](/docs/developers/api/templates) for the full envelope reference. ### Deprecated V1 API Endpoints [#deprecated-v1-api-endpoints] Full reference in the [V1 OpenAPI reference](https://openapi-v1.documenso.com). | Deprecated endpoint | Replacement | | -------------------------------------------------------- | ----------------------------------------------------- | | `GET /api/v1/documents` | `GET /api/v2/envelope` | | `GET /api/v1/documents/{id}` | `GET /api/v2/envelope/{envelopeId}` | | `POST /api/v1/documents` | `POST /api/v2/envelope/create` | | `POST /api/v1/documents/{id}/send` | `POST /api/v2/envelope/distribute` | | `POST /api/v1/documents/{id}/resend` | `POST /api/v2/envelope/redistribute` | | `DELETE /api/v1/documents/{id}` | `POST /api/v2/envelope/delete` | | `GET /api/v1/documents/{id}/download` | `GET /api/v2/envelope/item/{envelopeItemId}/download` | | `POST /api/v1/documents/{id}/recipients` | `POST /api/v2/envelope/recipient/create-many` | | `PATCH /api/v1/documents/{id}/recipients/{recipientId}` | `POST /api/v2/envelope/recipient/update-many` | | `DELETE /api/v1/documents/{id}/recipients/{recipientId}` | `POST /api/v2/envelope/recipient/delete` | | `POST /api/v1/documents/{id}/fields` | `POST /api/v2/envelope/field/create-many` | | `PATCH /api/v1/documents/{id}/fields/{fieldId}` | `POST /api/v2/envelope/field/update-many` | | `DELETE /api/v1/documents/{id}/fields/{fieldId}` | `POST /api/v2/envelope/field/delete` | | `GET /api/v1/templates` | `GET /api/v2/envelope` (with `type=TEMPLATE`) | | `GET /api/v1/templates/{id}` | `GET /api/v2/envelope/{envelopeId}` | | `POST /api/v1/templates` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) | | `DELETE /api/v1/templates/{id}` | `POST /api/v2/envelope/delete` | | `POST /api/v1/templates/{templateId}/create-document` | `POST /api/v2/envelope/use` | | `POST /api/v1/templates/{templateId}/generate-document` | `POST /api/v2/envelope/use` | ### Deprecated V2 API Endpoints [#deprecated-v2-api-endpoints] Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com). #### Documents [#documents] | Deprecated endpoint | Replacement | | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `GET /api/v2/document` | `GET /api/v2/envelope` | | `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` | | `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` (body changes from `documentIds: number[]` to `ids: { type: "documentId"; ids: number[] }`) | | `POST /api/v2/document/create` | `POST /api/v2/envelope/create` | | `POST /api/v2/document/create/beta` | `POST /api/v2/envelope/create` | | `POST /api/v2/document/update` | `POST /api/v2/envelope/update` | | `POST /api/v2/document/delete` | `POST /api/v2/envelope/delete` | | `POST /api/v2/document/duplicate` | `POST /api/v2/envelope/duplicate` | | `POST /api/v2/document/distribute` | `POST /api/v2/envelope/distribute` | | `POST /api/v2/document/redistribute` | `POST /api/v2/envelope/redistribute` | | `GET /api/v2/document/attachment` | `GET /api/v2/envelope/attachment` | | `POST /api/v2/document/attachment/create` | `POST /api/v2/envelope/attachment/create` | | `POST /api/v2/document/attachment/update` | `POST /api/v2/envelope/attachment/update` | | `POST /api/v2/document/attachment/delete` | `POST /api/v2/envelope/attachment/delete` | | `GET /api/v2/document/{documentId}/download` | `GET /api/v2/envelope/item/{envelopeItemId}/download` | | `GET /api/v2/document/{documentId}/download-beta` | `GET /api/v2/envelope/item/{envelopeItemId}/download` | #### Templates [#templates] | Deprecated endpoint | Replacement | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `GET /api/v2/template` | `GET /api/v2/envelope` (with `type=TEMPLATE`) | | `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` | | `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` (body changes from `templateIds: number[]` to `ids: { type: "templateId"; ids: number[] }`) | | `POST /api/v2/template/create` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) | | `POST /api/v2/template/create/beta` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) | | `POST /api/v2/template/update` | `POST /api/v2/envelope/update` | | `POST /api/v2/template/duplicate` | `POST /api/v2/envelope/duplicate` | | `POST /api/v2/template/delete` | `POST /api/v2/envelope/delete` | | `POST /api/v2/template/use` | `POST /api/v2/envelope/use` | | `POST /api/v2/template/direct/create` | **Pending replacement** | | `POST /api/v2/template/direct/delete` | **Pending replacement** | | `POST /api/v2/template/direct/toggle` | **Pending replacement** | #### Document fields [#document-fields] | Deprecated endpoint | Replacement | | ----------------------------------------- | ----------------------------------------- | | `GET /api/v2/document/field/{fieldId}` | `GET /api/v2/envelope/field/{fieldId}` | | `POST /api/v2/document/field/create` | `POST /api/v2/envelope/field/create-many` | | `POST /api/v2/document/field/create-many` | `POST /api/v2/envelope/field/create-many` | | `POST /api/v2/document/field/update` | `POST /api/v2/envelope/field/update-many` | | `POST /api/v2/document/field/update-many` | `POST /api/v2/envelope/field/update-many` | | `POST /api/v2/document/field/delete` | `POST /api/v2/envelope/field/delete` | #### Template fields [#template-fields] | Deprecated endpoint | Replacement | | ----------------------------------------- | ----------------------------------------- | | `GET /api/v2/template/field/{fieldId}` | `GET /api/v2/envelope/field/{fieldId}` | | `POST /api/v2/template/field/create` | `POST /api/v2/envelope/field/create-many` | | `POST /api/v2/template/field/create-many` | `POST /api/v2/envelope/field/create-many` | | `POST /api/v2/template/field/update` | `POST /api/v2/envelope/field/update-many` | | `POST /api/v2/template/field/update-many` | `POST /api/v2/envelope/field/update-many` | | `POST /api/v2/template/field/delete` | `POST /api/v2/envelope/field/delete` | #### Document recipients [#document-recipients] | Deprecated endpoint | Replacement | | ---------------------------------------------- | ---------------------------------------------- | | `GET /api/v2/document/recipient/{recipientId}` | `GET /api/v2/envelope/recipient/{recipientId}` | | `POST /api/v2/document/recipient/create` | `POST /api/v2/envelope/recipient/create-many` | | `POST /api/v2/document/recipient/create-many` | `POST /api/v2/envelope/recipient/create-many` | | `POST /api/v2/document/recipient/update` | `POST /api/v2/envelope/recipient/update-many` | | `POST /api/v2/document/recipient/update-many` | `POST /api/v2/envelope/recipient/update-many` | | `POST /api/v2/document/recipient/delete` | `POST /api/v2/envelope/recipient/delete` | #### Template recipients [#template-recipients] | Deprecated endpoint | Replacement | | ---------------------------------------------- | ---------------------------------------------- | | `GET /api/v2/template/recipient/{recipientId}` | `GET /api/v2/envelope/recipient/{recipientId}` | | `POST /api/v2/template/recipient/create` | `POST /api/v2/envelope/recipient/create-many` | | `POST /api/v2/template/recipient/create-many` | `POST /api/v2/envelope/recipient/create-many` | | `POST /api/v2/template/recipient/update` | `POST /api/v2/envelope/recipient/update-many` | | `POST /api/v2/template/recipient/update-many` | `POST /api/v2/envelope/recipient/update-many` | | `POST /api/v2/template/recipient/delete` | `POST /api/v2/envelope/recipient/delete` | ### Embedding components [#embedding-components] | Deprecated component | Replacement | | ----------------------- | --------------------- | | `EmbedCreateDocumentV1` | `EmbedCreateEnvelope` | | `EmbedCreateTemplateV1` | `EmbedCreateEnvelope` | | `EmbedUpdateDocumentV1` | `EmbedUpdateEnvelope` | | `EmbedUpdateTemplateV1` | `EmbedUpdateEnvelope` | See the [embedding guide](/docs/developers/embedding) for the envelope components. ## FAQ [#faq] The deprecated V1 API, the V2 endpoints listed above, and the V1 embedding components are removed. Requests to them will fail, so migrate to the envelope API before that date. Yes. Documents and templates you already created remain in your account and continue to work. They will automatically be converted to envelopes. Only the deprecated endpoints you call are going away. Your data is not deleted. No. Authentication is unchanged. The same API token works for the envelope endpoints under `https://app.documenso.com/api/v2`. Both are envelopes, distinguished by a `type` field of `DOCUMENT` or `TEMPLATE`. They share the same endpoints, recipients, fields, and attachments. The function calls to the legacy endpoints will break on the 1st of March 2027. Update to the latest SDK version and switch to its envelope methods. The deprecated document and template methods map to the envelope endpoints in the tables above. Reach out to [support@documenso.com](mailto:support@documenso.com) with your use case and we will help you plan the migration. ## Getting help [#getting-help] * [V2 OpenAPI reference](https://openapi.documenso.com): the up-to-date envelope API. * [V1 OpenAPI reference](https://openapi-v1.documenso.com): the deprecated V1 API. * [support@documenso.com](mailto:support@documenso.com): migration questions and extensions. ## See also [#see-also] * [Documents API](/docs/developers/api/documents): create and manage envelopes * [Templates API](/docs/developers/api/templates): work with templates and direct links * [Fields API](/docs/developers/api/fields) and [Recipients API](/docs/developers/api/recipients) * [API Versioning](/docs/developers/api/versioning): how Documenso versions the public API # Rate Limits ## Overview [#overview] Documenso enforces rate limits on all API endpoints to ensure service stability. ## HTTP Rate Limits [#http-rate-limits] The rate limit applies to: * `/api/v1/*` * `/api/v2/*` * `/api/v2-beta/*` **Limit:** 1000 requests per minute per IP address\ **Response:** 429 Too Many Requests This is the global per-IP ceiling. Your organisation may have its own rate limits configured below this value, in which case you can be rate-limited before reaching the global limit. ### Global per-IP 429 Response [#global-per-ip-429-response] ```json { "error": "Too many requests, please try again later." } ``` ### Rate Limit Headers [#rate-limit-headers] Responses from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` include these headers. The only exception is CORS preflight (`OPTIONS`) requests, which are answered before the rate limiter runs and carry no rate limit headers: | Header | Description | | ----------------------- | ---------------------------------------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed in the current global window | | `X-RateLimit-Remaining` | Requests remaining in the current global window | | `X-RateLimit-Reset` | End of the current global window, as a Unix epoch timestamp in seconds | A 429 response from a windowed limiter also includes `Retry-After`, in seconds, with a minimum value of `1`. The global API limit uses fixed, epoch-aligned one-minute buckets, so the actual wait until the next window is between 1 and 60 seconds. Honor `Retry-After` exactly instead of sleeping for a fixed 60 seconds. See the [Retry-After handling example](/docs/developers/examples/common-workflows#error-handling-patterns). ## Resource Limits [#resource-limits] Beyond HTTP rate limits, your account has usage limits based on your subscription plan. ### Plan Limits [#plan-limits] | Resource | Free | Paid | Self-hosted | Enterprise | | ---------------- | ---- | --------- | ----------- | ---------- | | Documents/month | 5 | Unlimited | Unlimited | Unlimited | | Total Recipients | 10 | Unlimited | Unlimited | Unlimited | | Direct Templates | 3 | Unlimited | Unlimited | Unlimited | ### Organisation Limit 429 Responses [#organisation-limit-429-responses] Organisation windowed limits and organisation monthly quotas produce 429 responses whose body shape depends on the API version, and neither matches the global per-IP limiter's `{ "error": "..." }` body. On `/api/v1/*`, the body contains only a message: ```json { "message": "Too many requests, please try again later. Contact support if you require higher limits." } ``` On `/api/v2/*` and `/api/v2-beta/*`, the body is a structured error object: ```json { "message": "Too many requests, please try again later. Contact support if you require higher limits.", "code": "TOO_MANY_REQUESTS", "data": { "code": "TOO_MANY_REQUESTS", "httpStatus": 429, "appError": { "code": "TOO_MANY_REQUESTS", "message": "Too many requests, please try again later. Contact support if you require higher limits." } } } ``` Organisation windowed limit responses include the `X-RateLimit-*` headers and `Retry-After` for their own window. Monthly quota responses carry no quota-specific rate limit headers or `Retry-After` because the quota is not a time window; rely on the status code and message instead. ## Error Codes [#error-codes] | Code | Status | Description | | ------------------- | ------ | --------------------------------------------------------------- | | `TOO_MANY_REQUESTS` | 429 | Global per-IP, organisation windowed, or monthly quota exceeded | | `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded | There are three sources of `TOO_MANY_REQUESTS` responses: 1. The global per-IP limit, returning the `{ "error": "..." }` body shown above. 2. Organisation windowed rate limits for the `api`, `document`, and `email` counters. 3. Organisation monthly quotas for the same three counters. Every authenticated API request consumes the `api` counter, so any endpoint can return this 429 once the monthly API quota is exhausted — not just envelope-related ones. *** ## See Also [#see-also] * [Authentication](/docs/developers/getting-started/authentication) - API authentication guide * [API Versioning](/docs/developers/api/versioning) - API version management * [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API * [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Admins: set per-organisation resource quotas and rate limits (the HTTP rate limit above is separate and not admin-settable) # Recipients API This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). ## Recipient Object [#recipient-object] ```json { "id": 123, "envelopeId": "clu1abc2def3ghi4jkl", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "signingOrder": 1, "token": "abc123...", "signedAt": "2024-01-15T10:30:00Z", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" } ``` | Field | Type | Description | | --------------- | -------------- | ------------------------------------- | | `id` | number | Unique recipient identifier | | `envelopeId` | string | ID of the associated envelope | | `email` | string | Recipient's email address | | `name` | string | Recipient's display name | | `role` | string | Recipient role (see below) | | `signingOrder` | number \| null | Order in sequential signing | | `token` | string | Unique token for signing URL | | `signedAt` | string \| null | ISO timestamp when signed | | `readStatus` | string | `NOT_OPENED` or `OPENED` | | `signingStatus` | string | `NOT_SIGNED`, `SIGNED`, or `REJECTED` | | `sendStatus` | string | `NOT_SENT` or `SENT` | *** ## Recipient Roles [#recipient-roles] | Role | Description | | ----------- | -------------------------------------------------------------- | | `SIGNER` | Must sign the document. Required fields must be completed. | | `APPROVER` | Must approve the document before signers can proceed. | | `VIEWER` | Can view the document but takes no action. | | `CC` | Receives a copy of the completed document. No action required. | | `ASSISTANT` | Can fill in fields on behalf of another recipient. | *** ## Get Recipient [#get-recipient] Retrieve a single recipient by ID. ``` GET /api/v2/envelope/recipient/{recipientId} ``` ### Example [#example] ```bash curl "https://app.documenso.com/api/v2/envelope/recipient/789" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/recipient/789', { headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, }, ); const recipient = await response.json(); ``` ### Response [#response] Returns the full recipient object including fields. *** ## Create Recipients [#create-recipients] Add one or more recipients to an envelope. ``` POST /api/v2/envelope/recipient/create-many ``` ### Request Body [#request-body] | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------- | | `envelopeId` | string | Yes | ID of the envelope to add recipients to | | `data` | array | Yes | Array of recipient objects | Each item in the `data` array: | Field | Type | Required | Description | | -------------- | --------- | -------- | ------------------------------------------ | | `email` | string | Yes | Recipient's email address | | `name` | string | Yes | Recipient's display name (max 255 chars) | | `role` | string | Yes | Recipient role (see Recipient Roles above) | | `signingOrder` | number | No | Position in sequential signing | | `accessAuth` | string\[] | No | Access authentication types | | `actionAuth` | string\[] | No | Action authentication types | ### Example [#example-1] ```bash 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", "data": [ { "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "signingOrder": 1 }, { "email": "approver@example.com", "name": "Jane Smith", "role": "APPROVER", "signingOrder": 0 } ] }' ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/recipient/create-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'clu1abc2def3ghi4jkl', data: [ { email: 'signer@example.com', name: 'John Doe', role: 'SIGNER', signingOrder: 1, }, { email: 'approver@example.com', name: 'Jane Smith', role: 'APPROVER', signingOrder: 0, }, ], }), }, ); const { data: recipients } = await response.json(); ``` ### Response [#response-1] ```json { "data": [ { "id": 789, "envelopeId": "clu1abc2def3ghi4jkl", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "signingOrder": 1, "token": "abc123def456", "signedAt": null, "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "NOT_SENT" }, { "id": 790, "envelopeId": "clu1abc2def3ghi4jkl", "email": "approver@example.com", "name": "Jane Smith", "role": "APPROVER", "signingOrder": 0, "token": "def456ghi789", "signedAt": null, "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "NOT_SENT" } ] } ``` *** ## Update Recipients [#update-recipients] Update one or more recipients on an envelope. Only available for envelopes that are not yet completed. ``` POST /api/v2/envelope/recipient/update-many ``` ### Request Body [#request-body-1] | Field | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------- | | `envelopeId` | string | Yes | ID of the envelope containing the recipients | | `data` | array | Yes | Array of recipient update objects | Each item in the `data` array: | Field | Type | Required | Description | | -------------- | --------- | -------- | ---------------------------------- | | `id` | number | Yes | ID of the recipient to update | | `email` | string | No | New email address | | `name` | string | No | New display name (max 255 chars) | | `role` | string | No | New recipient role | | `signingOrder` | number | No | New position in sequential signing | | `accessAuth` | string\[] | No | Access authentication types | | `actionAuth` | string\[] | No | Action authentication types | ### Example [#example-2] ```bash 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", "data": [ { "id": 789, "name": "Jane Doe", "signingOrder": 2 } ] }' ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/recipient/update-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'clu1abc2def3ghi4jkl', data: [ { id: 789, name: 'Jane Doe', signingOrder: 2, }, ], }), }, ); const { data: updatedRecipients } = await response.json(); ``` ### Response [#response-2] Returns the updated recipient objects in a `data` array. *** ## Delete Recipient [#delete-recipient] Remove a recipient from an envelope. Only available for envelopes that are not yet completed. ``` POST /api/v2/envelope/recipient/delete ``` ### Request Body [#request-body-2] | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------- | | `recipientId` | number | Yes | ID of the recipient to remove | ### Example [#example-3] ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/delete" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipientId": 789 }' ``` ```typescript const response = await fetch( 'https://app.documenso.com/api/v2/envelope/recipient/delete', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ recipientId: 789, }), }, ); const result = await response.json(); // { "success": true } ``` ### Response [#response-3] ```json { "success": true } ``` *** ## Signing Order [#signing-order] When an envelope uses sequential signing, recipients sign in a specific order defined by `signingOrder`. ### Setting Signing Order [#setting-signing-order] When creating recipients, assign `signingOrder` values to control the sequence: ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope/recipient/create-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'clu1abc2def3ghi4jkl', data: [ { email: 'approver@example.com', name: 'Approver', role: 'APPROVER', signingOrder: 0, // Approvers typically go first }, { email: 'first@example.com', name: 'First Signer', role: 'SIGNER', signingOrder: 1, }, { email: 'second@example.com', name: 'Second Signer', role: 'SIGNER', signingOrder: 2, }, ], }), }); ``` To enable sequential signing, set `signingOrder` to `SEQUENTIAL` in the envelope metadata when creating or updating the envelope. See the [Documents API](/docs/developers/api/documents) for details. ### Signing Order Behavior [#signing-order-behavior] * Recipients with lower `signingOrder` values sign first * Recipients with the same `signingOrder` can sign simultaneously * `CC` recipients receive the document after all signing is complete * `APPROVER` recipients must approve before signers with higher order values *** ## Authentication Options [#authentication-options] For enhanced security, you can require additional authentication when recipients access or sign a document. ### Access Authentication [#access-authentication] Controls who can view the document: | Type | Description | | ----------------- | ------------------------------ | | `ACCOUNT` | Recipient must be logged in | | `TWO_FACTOR_AUTH` | Recipient must verify with 2FA | ### Action Authentication [#action-authentication] Controls who can sign the document: | Type | Description | | ----------------- | ---------------------------------------- | | `ACCOUNT` | Recipient must be logged in | | `PASSKEY` | Require passkey authentication | | `TWO_FACTOR_AUTH` | Require 2FA code | | `PASSWORD` | Require password verification | | `EXPLICIT_NONE` | Explicitly disable action authentication | ### Example [#example-4] ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope/recipient/create-many', { method: 'POST', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ envelopeId: 'clu1abc2def3ghi4jkl', data: [ { email: 'signer@example.com', name: 'John Doe', role: 'SIGNER', accessAuth: ['ACCOUNT'], actionAuth: ['PASSKEY', 'TWO_FACTOR_AUTH'], }, ], }), }); ``` *** ## Error Responses [#error-responses] | Status | Description | | ------ | ------------------------------------------------ | | `400` | Invalid request body or recipient already exists | | `400` | Envelope is already completed | | `401` | Invalid or missing API key | | `404` | Envelope or recipient not found | | `500` | Server error | ### Example Error Response [#example-error-response] ```json { "message": "Recipient already exists" } ``` *** ## See Also [#see-also] * [Documents API](/docs/developers/api/documents) - Create and manage envelopes * [Fields API](/docs/developers/api/fields) - Add signature fields for recipients # Teams API This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). ## Team Object [#team-object] A team object contains the following properties: | Property | Type | Description | | ----------------- | -------------- | --------------------------------------------------- | | `id` | number | Unique team identifier | | `name` | string | Team display name | | `url` | string | Unique team URL slug | | `createdAt` | string | ISO 8601 timestamp | | `avatarImageId` | string \| null | ID of the team's avatar image | | `organisationId` | string | ID of the parent organisation | | `currentTeamRole` | string | Your role in the team: `ADMIN`, `MANAGER`, `MEMBER` | ### Example Team Object [#example-team-object] ```json { "id": 123, "name": "Engineering", "url": "engineering", "createdAt": "2025-01-15T10:30:00.000Z", "avatarImageId": null, "organisationId": "org_abc123", "currentTeamRole": "ADMIN" } ``` ## Team-Scoped API Tokens [#team-scoped-api-tokens] API tokens in Documenso are always scoped to a specific team. When you create an API token, it is associated with the team you're currently working in. ### How Team Scoping Works [#how-team-scoping-works] * Each API token belongs to exactly one team * All API operations using that token automatically access that team's resources * Documents, templates, and other resources created via the API belong to the token's team * You cannot access resources from other teams with a single token ### Creating Team-Scoped Tokens [#creating-team-scoped-tokens] {/* prettier-ignore */} Navigate to your team's settings Go to **API Tokens** Click **Create Token** The token will be scoped to the current team To work with multiple teams via API, create separate tokens for each team. ### Token Permissions [#token-permissions] Your API token inherits permissions based on your role in the team: | Role | Permissions | | --------- | ------------------------------------------------ | | `ADMIN` | Full access to all team resources and settings | | `MANAGER` | Create, edit, and delete documents and templates | | `MEMBER` | Create and manage own documents | ## Working with Team Documents [#working-with-team-documents] When you use a team-scoped API token, all document operations are automatically scoped to that team. ### Create a Team Document [#create-a-team-document] Documents created with a team token belong to that team: ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/create" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: multipart/form-data" \ -F 'payload={ "type": "DOCUMENT", "title": "Team Contract", "recipients": [ { "email": "signer@example.com", "name": "John Smith", "role": "SIGNER" } ] }' \ -F "files=@./contract.pdf;type=application/pdf" ``` ```typescript const TEAM_API_TOKEN = process.env.DOCUMENSO_TEAM_API_TOKEN; const form = new FormData(); const payload = { type: 'DOCUMENT', title: 'Team Contract', recipients: [ { email: 'signer@example.com', name: 'John Smith', role: 'SIGNER', }, ], }; form.append('payload', JSON.stringify(payload)); form.append('files', fs.createReadStream('./contract.pdf'), { contentType: 'application/pdf', }); const response = await fetch('https://app.documenso.com/api/v2/envelope/create', { method: 'POST', headers: { Authorization: TEAM_API_TOKEN, }, body: form, }); const { id } = await response.json(); console.log('Created team document:', id); ``` ### List Team Documents [#list-team-documents] Retrieve all documents belonging to the team: ```bash # List all team documents curl -X GET "https://app.documenso.com/api/v2/envelope" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" # Filter by status curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const response = await fetch('https://app.documenso.com/api/v2/envelope', { method: 'GET', headers: { Authorization: TEAM_API_TOKEN, }, }); const { data, pagination } = await response.json(); console.log(`Found ${pagination.totalItems} team documents`); ``` ## Working with Team Templates [#working-with-team-templates] Templates created with a team token are shared across the team. ### Create a Team Template [#create-a-team-template] ```bash curl -X POST "https://app.documenso.com/api/v2/template/create" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: multipart/form-data" \ -F 'payload={ "title": "NDA Template", "recipients": [ { "email": "placeholder@example.com", "name": "Signer", "role": "SIGNER", "fields": [ { "identifier": 0, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5 } ] } ] }' \ -F "files=@./nda-template.pdf;type=application/pdf" ``` ```typescript const form = new FormData(); const payload = { title: 'NDA Template', recipients: [ { email: 'placeholder@example.com', name: 'Signer', role: 'SIGNER', fields: [ { identifier: 0, type: 'SIGNATURE', page: 1, positionX: 10, positionY: 80, width: 30, height: 5, }, ], }, ], }; form.append('payload', JSON.stringify(payload)); form.append('files', fs.createReadStream('./nda-template.pdf'), { contentType: 'application/pdf', }); const response = await fetch('https://app.documenso.com/api/v2/template/create', { method: 'POST', headers: { Authorization: TEAM_API_TOKEN, }, body: form, }); const template = await response.json(); console.log('Created team template:', template.id); ``` ### List Team Templates [#list-team-templates] ```bash curl -X GET "https://app.documenso.com/api/v2/template" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const response = await fetch('https://app.documenso.com/api/v2/template', { method: 'GET', headers: { Authorization: TEAM_API_TOKEN, }, }); const { data } = await response.json(); console.log('Team templates:', data); ``` ## Team Member Roles [#team-member-roles] | Role | Description | | --------- | ----------------------------------------------------------- | | `ADMIN` | Full control over team settings, members, and all resources | | `MANAGER` | Can manage documents, templates, and view team resources | | `MEMBER` | Can create and manage their own documents within the team | ### Document Visibility [#document-visibility] Team documents have visibility settings that control who can access them: | Visibility | Description | | ------------------- | -------------------------------------- | | `EVERYONE` | All team members can view the document | | `MANAGER_AND_ABOVE` | Only managers and admins can view | | `ADMIN` | Only admins can view | Set visibility when creating a document: ```typescript const payload = { type: 'DOCUMENT', title: 'Confidential Agreement', visibility: 'ADMIN', // Only team admins can view recipients: [...], }; ``` ## Multi-Team Workflow [#multi-team-workflow] To work with multiple teams, create and manage separate API tokens for each team. ### Example: Sync Documents Across Teams [#example-sync-documents-across-teams] ```typescript // Tokens for different teams const SALES_TEAM_TOKEN = process.env.SALES_TEAM_API_TOKEN; const LEGAL_TEAM_TOKEN = process.env.LEGAL_TEAM_API_TOKEN; // Get pending documents from sales team const salesResponse = await fetch('https://app.documenso.com/api/v2/envelope?status=PENDING', { headers: { Authorization: SALES_TEAM_TOKEN }, }); const salesDocs = await salesResponse.json(); // Get completed documents from legal team const legalResponse = await fetch('https://app.documenso.com/api/v2/envelope?status=COMPLETED', { headers: { Authorization: LEGAL_TEAM_TOKEN }, }); const legalDocs = await legalResponse.json(); console.log(`Sales team: ${salesDocs.pagination.totalItems} pending`); console.log(`Legal team: ${legalDocs.pagination.totalItems} completed`); ``` ## Error Responses [#error-responses] | Status | Description | | ------ | ------------------------------------------------- | | `401` | Invalid or expired API token | | `403` | Token doesn't have permission for this operation | | `404` | Resource not found or not accessible by this team | ### Example Error Response [#example-error-response] ```json { "message": "You do not have permission to access this resource" } ``` API tokens can only access resources belonging to their associated team. Attempting to access resources from another team returns a 403 or 404 error. ## See Also [#see-also] * [Documents API](/docs/developers/api/documents) - Create and manage documents * [Templates API](/docs/developers/api/templates) - Work with document templates * [Authentication](/docs/developers/getting-started/authentication) - Create and manage API tokens # Templates API This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). ## Template Object [#template-object] A template object contains the following properties: | Property | Type | Description | | ------------------- | -------------- | ------------------------------------------------------ | | `id` | number | Unique template identifier | | `envelopeId` | string | Associated envelope ID | | `type` | string | Template type: `PRIVATE` or `PUBLIC` | | `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` | | `title` | string | Template title | | `externalId` | string \| null | Your custom identifier | | `publicTitle` | string \| null | Public-facing title (for public templates) | | `publicDescription` | string \| null | Public-facing description (for public templates) | | `userId` | number | Owner's user ID | | `teamId` | number \| null | Team ID if team-owned | | `folderId` | string \| null | Folder containing the template | | `createdAt` | string | ISO 8601 timestamp | | `updatedAt` | string | ISO 8601 timestamp | | `recipients` | array | Predefined recipients with roles and fields | | `fields` | array | Signature and form fields on the template | | `templateMeta` | object | Email settings, signing options, redirect URL | | `directLink` | object \| null | Direct link configuration if enabled | ### Example Template Object [#example-template-object] ```json { "id": 123, "envelopeId": "envelope_abc123xyz", "type": "PRIVATE", "visibility": "EVERYONE", "title": "Employment Contract", "externalId": "template-emp-001", "publicTitle": null, "publicDescription": null, "userId": 1, "teamId": 5, "folderId": null, "createdAt": "2025-01-10T08:00:00.000Z", "updatedAt": "2025-01-10T08:30:00.000Z", "recipients": [ { "id": 1, "email": "employee@example.com", "name": "Employee", "role": "SIGNER", "signingOrder": 1 }, { "id": 2, "email": "hr@company.com", "name": "HR Manager", "role": "SIGNER", "signingOrder": 2 } ], "fields": [ { "id": 101, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5, "recipientId": 1 } ], "templateMeta": { "subject": "Your Employment Contract", "message": "Please review and sign your employment contract.", "redirectUrl": "https://example.com/welcome" }, "directLink": null } ``` ## List Templates [#list-templates] Retrieve a paginated list of templates. ``` GET /template ``` ### Query Parameters [#query-parameters] | Parameter | Type | Description | | ------------------ | ------- | ------------------------------------------------- | | `page` | integer | Page number (default: 1) | | `perPage` | integer | Results per page (default: 10, max: 100) | | `type` | string | Filter by type: `PRIVATE` or `PUBLIC` | | `folderId` | string | Filter by folder ID | | `orderByColumn` | string | Sort field (only `createdAt` supported) | | `orderByDirection` | string | Sort direction: `asc` or `desc` (default: `desc`) | ### Code Examples [#code-examples] ```bash # List all templates curl -X GET "https://app.documenso.com/api/v2/template" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" # Filter by type and paginate curl -X GET "https://app.documenso.com/api/v2/template?type=PRIVATE&page=1&perPage=20" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; // List all templates const response = await fetch(`${BASE_URL}/template`, { method: 'GET', headers: { Authorization: API_TOKEN, }, }); const { data, pagination } = await response.json(); console.log(`Found ${pagination.totalItems} templates`); // Filter by type const privateResponse = await fetch( `${BASE_URL}/template?type=PRIVATE&page=1&perPage=20`, { method: 'GET', headers: { Authorization: API_TOKEN, }, } ); const privateTemplates = await privateResponse.json(); ``` ### Response [#response] ```json { "data": [ { "id": 123, "envelopeId": "envelope_abc123", "type": "PRIVATE", "title": "Employment Contract", "createdAt": "2025-01-10T08:00:00.000Z", "updatedAt": "2025-01-10T08:30:00.000Z", "recipients": [ { "id": 1, "email": "employee@example.com", "name": "Employee", "role": "SIGNER" } ] } ], "pagination": { "page": 1, "perPage": 10, "totalPages": 3, "totalItems": 25 } } ``` *** ## Get Template [#get-template] Retrieve a single template by ID. ``` GET /template/{templateId} ``` ### Path Parameters [#path-parameters] | Parameter | Type | Description | | ------------ | ------ | --------------- | | `templateId` | number | The template ID | ### Code Examples [#code-examples-1] ```bash curl -X GET "https://app.documenso.com/api/v2/template/123" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ```typescript const templateId = 123; const response = await fetch(`https://app.documenso.com/api/v2/template/${templateId}`, { method: 'GET', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, }); const template = await response.json(); console.log(template.title, template.recipients.length); ``` ### Response [#response-1] Returns the full template object including recipients, fields, and metadata. *** ## Create Document from Template [#create-document-from-template] Create a new document using a template. This is the primary way to use templates programmatically. This endpoint does not support [PDF placeholder parsing](/docs/users/documents/advanced/pdf-placeholders). Use `POST /envelope/create` for placeholder-based field positioning. ``` POST /template/use ``` ### Request Body [#request-body] | Field | Type | Required | Description | | -------------------- | ------- | -------- | ------------------------------------------------------ | | `templateId` | number | Yes | The template ID to use | | `recipients` | array | Yes | Recipient details (maps to template recipients) | | `distributeDocument` | boolean | No | If `true`, immediately send the document to recipients | | `externalId` | string | No | Your custom identifier for the created document | | `folderId` | string | No | Folder ID to create the document in | | `prefillFields` | array | No | Pre-fill field values before sending | | `override` | object | No | Override template settings for this document | | `formValues` | object | No | PDF form values to insert | ### Recipients Array [#recipients-array] Each recipient object must include: | Field | Type | Required | Description | | ------- | ------ | -------- | ---------------------------------- | | `id` | number | Yes | The recipient ID from the template | | `email` | string | Yes | Recipient's email address | | `name` | string | No | Recipient's display name | ### Code Examples [#code-examples-2] ```bash # Create document from template curl -X POST "https://app.documenso.com/api/v2/template/use" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123, "recipients": [ { "id": 1, "email": "john.doe@example.com", "name": "John Doe" }, { "id": 2, "email": "jane.smith@company.com", "name": "Jane Smith" } ], "distributeDocument": true, "externalId": "contract-2025-001" }' # Create document with field prefilling curl -X POST "https://app.documenso.com/api/v2/template/use" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123, "recipients": [ { "id": 1, "email": "john.doe@example.com", "name": "John Doe" } ], "prefillFields": [ { "id": 101, "type": "text", "value": "Senior Software Engineer" }, { "id": 102, "type": "number", "value": "85000" }, { "id": 103, "type": "date", "value": "2025-02-01" } ], "distributeDocument": true }' ``` ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; // Create document from template const response = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, recipients: [ { id: 1, email: 'john.doe@example.com', name: 'John Doe', }, { id: 2, email: 'jane.smith@company.com', name: 'Jane Smith', }, ], distributeDocument: true, externalId: 'contract-2025-001', }), }); const document = await response.json(); console.log('Created document:', document.id); // Create document with prefilled fields const prefillResponse = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, recipients: [ { id: 1, email: 'john.doe@example.com', name: 'John Doe', }, ], prefillFields: [ { id: 101, type: 'text', value: 'Senior Software Engineer', }, { id: 102, type: 'number', value: '85000', }, { id: 103, type: 'date', value: '2025-02-01', }, ], distributeDocument: true, }), }); const prefilledDocument = await prefillResponse.json(); ``` ### Response [#response-2] Returns the created document object with recipients and signing URLs. ```json { "id": "envelope_xyz789", "type": "DOCUMENT", "status": "PENDING", "title": "Employment Contract", "source": "TEMPLATE", "externalId": "contract-2025-001", "recipients": [ { "id": 1, "email": "john.doe@example.com", "name": "John Doe", "role": "SIGNER", "signingStatus": "NOT_SIGNED", "signingUrl": "https://app.documenso.com/sign/abc123" } ] } ``` *** ## Override Template Settings [#override-template-settings] When creating a document from a template, you can override various settings: ```typescript const response = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, recipients: [{ id: 1, email: 'signer@example.com', name: 'Signer' }], override: { title: 'Custom Document Title', subject: 'Custom email subject', message: 'Custom email message body', timezone: 'America/New_York', dateFormat: 'MM/dd/yyyy', redirectUrl: 'https://example.com/custom-redirect', language: 'de', typedSignatureEnabled: true, uploadSignatureEnabled: false, drawSignatureEnabled: true, }, distributeDocument: true, }), }); ``` ### Override Options [#override-options] | Field | Type | Description | | ------------------------ | ------- | ------------------------------- | | `title` | string | Document title | | `subject` | string | Email subject line | | `message` | string | Email body message | | `timezone` | string | Timezone for date fields | | `dateFormat` | string | Date format string | | `redirectUrl` | string | URL to redirect after signing | | `language` | string | Document language code | | `typedSignatureEnabled` | boolean | Allow typed signatures | | `uploadSignatureEnabled` | boolean | Allow uploaded signature images | | `drawSignatureEnabled` | boolean | Allow drawn signatures | *** ## Prefill Fields [#prefill-fields] Prefill field values when creating a document from a template. This is useful for populating known data before sending. ### Supported Field Types [#supported-field-types] | Type | Value Format | Description | | ---------- | ----------------- | ------------------------------ | | `text` | string | Text field value | | `number` | string | Numeric value as string | | `date` | string (ISO 8601) | Date in `YYYY-MM-DD` format | | `radio` | string | Selected option value | | `checkbox` | array of strings | Array of checked option values | | `dropdown` | string | Selected dropdown value | ### Prefill Field Schema [#prefill-field-schema] ```typescript type PrefillField = { id: number; // Field ID from template type: string; // Field type (must match template field) label?: string; // Optional label override placeholder?: string; // Optional placeholder (text/number only) value: string | string[]; // Value to prefill }; ``` ### Example: Prefill Multiple Field Types [#example-prefill-multiple-field-types] ```typescript const response = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, recipients: [{ id: 1, email: 'signer@example.com', name: 'Signer' }], prefillFields: [ // Text field { id: 101, type: 'text', value: 'John Doe', }, // Number field { id: 102, type: 'number', value: '50000', }, // Date field { id: 103, type: 'date', value: '2025-03-15', }, // Radio field (select one option) { id: 104, type: 'radio', value: 'full-time', }, // Checkbox field (select multiple options) { id: 105, type: 'checkbox', value: ['health', 'dental', '401k'], }, // Dropdown field { id: 106, type: 'dropdown', value: 'engineering', }, ], distributeDocument: true, }), }); ``` The field type in `prefillFields` must match the actual field type in the template. A mismatch will result in an error. *** ## Update Template [#update-template] Update a template's properties. ``` POST /template/update ``` ### Request Body [#request-body-1] | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------ | | `templateId` | number | Yes | Template ID | | `data` | object | No | Template properties to update | | `meta` | object | No | Email and signing settings to update | ### Code Examples [#code-examples-3] ```bash curl -X POST "https://app.documenso.com/api/v2/template/update" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123, "data": { "title": "Updated Employment Contract", "visibility": "ADMIN" }, "meta": { "subject": "Updated: Your Employment Contract", "redirectUrl": "https://example.com/updated-redirect" } }' ``` ```typescript const response = await fetch(`${BASE_URL}/template/update`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, data: { title: 'Updated Employment Contract', visibility: 'ADMIN', }, meta: { subject: 'Updated: Your Employment Contract', redirectUrl: 'https://example.com/updated-redirect', }, }), }); const template = await response.json(); ``` *** ## Duplicate Template [#duplicate-template] Create a copy of an existing template. ``` POST /template/duplicate ``` ### Request Body [#request-body-2] | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `templateId` | number | Yes | Template ID | ### Code Examples [#code-examples-4] ```bash curl -X POST "https://app.documenso.com/api/v2/template/duplicate" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123 }' ``` ```typescript const response = await fetch(`${BASE_URL}/template/duplicate`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, }), }); const duplicatedTemplate = await response.json(); console.log('New template ID:', duplicatedTemplate.id); ``` *** ## Delete Template [#delete-template] Delete a template. ``` POST /template/delete ``` ### Request Body [#request-body-3] | Field | Type | Required | Description | | ------------ | ------ | -------- | ----------- | | `templateId` | number | Yes | Template ID | ### Code Examples [#code-examples-5] ```bash curl -X POST "https://app.documenso.com/api/v2/template/delete" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123 }' ``` ```typescript const response = await fetch(`${BASE_URL}/template/delete`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, }), }); const { success } = await response.json(); ``` ### Response [#response-3] ```json { "success": true } ``` *** ## Direct Link Templates [#direct-link-templates] Direct link templates allow recipients to create and sign documents without requiring you to explicitly create each document. When a recipient visits the direct link, a new document is automatically created from the template. ### Create Direct Link [#create-direct-link] ``` POST /template/direct/create ``` | Field | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------------- | | `templateId` | number | Yes | Template ID | | `directRecipientId` | number | No | Recipient ID to use as the direct link signer | ```bash curl -X POST "https://app.documenso.com/api/v2/template/direct/create" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123, "directRecipientId": 1 }' ``` ```typescript const response = await fetch(`${BASE_URL}/template/direct/create`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, directRecipientId: 1, }), }); const directLink = await response.json(); console.log('Direct link token:', directLink.token); // Share: https://app.documenso.com/d/{token} ``` ### Response [#response-4] ```json { "id": 456, "token": "abc123xyz", "templateId": 123, "directTemplateRecipientId": 1, "enabled": true, "createdAt": "2025-01-15T10:00:00.000Z" } ``` ### Toggle Direct Link [#toggle-direct-link] Enable or disable an existing direct link. ``` POST /template/direct/toggle ``` ```bash curl -X POST "https://app.documenso.com/api/v2/template/direct/toggle" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123, "enabled": false }' ``` ```typescript const response = await fetch(`${BASE_URL}/template/direct/toggle`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, enabled: false, }), }); const directLink = await response.json(); console.log('Direct link enabled:', directLink.enabled); ``` ### Delete Direct Link [#delete-direct-link] ``` POST /template/direct/delete ``` ```bash curl -X POST "https://app.documenso.com/api/v2/template/direct/delete" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123 }' ``` ```typescript const response = await fetch(`${BASE_URL}/template/direct/delete`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, }), }); const { success } = await response.json(); ``` *** ## Custom Document Data [#custom-document-data] When creating a document from a template, you can replace the template's PDF with a custom PDF by using the `customDocumentData` parameter. This is useful when you need to generate the PDF dynamically while reusing the template's recipient and field configuration. To use custom document data: 1. Retrieve the template to get the `envelopeItemId` of the PDF you want to replace 2. Include `customDocumentData` in your `template/use` request, mapping each `envelopeItemId` to a new file The field positions from the template are preserved on the new PDF. Ensure the replacement PDF has the same page layout to maintain accurate field placement. See the [OpenAPI Reference](https://openapi.documenso.com) for the full request schema. *** ## Template Types [#template-types] | Type | Description | | --------- | ----------------------------------------- | | `PRIVATE` | Only accessible by owner and team members | | `PUBLIC` | Can be shared publicly with a direct link | *** ## Complete Example: Contract Workflow [#complete-example-contract-workflow] This example demonstrates a complete workflow for using templates to send contracts. ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; async function sendEmploymentContract(employeeData: { email: string; name: string; position: string; salary: number; startDate: string; }) { // 1. Get the template to find recipient and field IDs const templateResponse = await fetch(`${BASE_URL}/template/456`, { headers: { Authorization: API_TOKEN }, }); const template = await templateResponse.json(); // 2. Find the employee recipient slot const employeeRecipient = template.recipients.find( (r) => r.role === 'SIGNER' && r.signingOrder === 1 ); // 3. Find fields to prefill const positionField = template.fields.find( (f) => f.fieldMeta?.label === 'Position' ); const salaryField = template.fields.find( (f) => f.fieldMeta?.label === 'Salary' ); const startDateField = template.fields.find( (f) => f.type === 'DATE' ); // 4. Create document from template with prefilled data const documentResponse = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 456, recipients: [ { id: employeeRecipient.id, email: employeeData.email, name: employeeData.name, }, ], prefillFields: [ { id: positionField.id, type: 'text', value: employeeData.position, }, { id: salaryField.id, type: 'number', value: employeeData.salary.toString(), }, { id: startDateField.id, type: 'date', value: employeeData.startDate, }, ], override: { subject: `Employment Contract for ${employeeData.name}`, message: `Hi ${employeeData.name},\n\nPlease review and sign your employment contract.`, }, distributeDocument: true, externalId: `emp-contract-${Date.now()}`, }), }); const document = await documentResponse.json(); return { documentId: document.id, signingUrl: document.recipients[0].signingUrl, }; } // Usage const result = await sendEmploymentContract({ email: 'john.doe@example.com', name: 'John Doe', position: 'Senior Engineer', salary: 120000, startDate: '2025-03-01', }); console.log('Document created:', result.documentId); console.log('Signing URL:', result.signingUrl); ``` *** ## See Also [#see-also] * [Documents API](/docs/developers/api/documents) - Work with created documents * [Recipients API](/docs/developers/api/recipients) - Manage document recipients * [Fields API](/docs/developers/api/fields) - Work with signature and form fields * [Webhooks](/docs/developers/webhooks) - Get notified when documents are signed # API Versioning ## Overview [#overview] Documenso uses API versioning to manage changes to the public API. This allows us to introduce new features, fix bugs, and make other changes without breaking existing integrations. The current version of the API is `v2` . The API version is specified in the URL. For example, the base URL for the `v2` API is `https://app.documenso.com/api/v2`. We may make changes to the API without incrementing the version number. We will always try to avoid breaking changes, but in some cases, it may be necessary to make changes that are not backward compatible. In these cases, we will increment the version number and provide information about the changes in the release notes. Also, we may deprecate certain features or endpoints in the API. When we deprecate a feature or endpoint, we will provide information about the deprecation in the release notes and give a timeline for when the feature or endpoint will be removed. *** ## Documents, Templates, and Envelopes [#documents-templates-and-envelopes] Documenso has unified documents and templates into a single resource called an **envelope**. New integrations should create documents and templates through the `/envelope/*` endpoints. The `POST /document/create` and `POST /template/create` endpoints (including their `/beta` variants) are deprecated in favor of `POST /envelope/create`. See [Migrating to the Envelope API](/docs/developers/api/migrate-to-envelopes) for the rationale and step-by-step migration examples. *** ## See Also [#see-also] * [Migrating to the Envelope API](/docs/developers/api/migrate-to-envelopes) - Move from the document and template create endpoints * [Authentication](/docs/developers/getting-started/authentication) - API authentication guide * [Rate Limits](/docs/developers/api/rate-limits) - API rate limit details # Demo Environment ## Overview [#overview] The demo (staging) environment is a sandbox environment that replicates the production environment. It has the same features and capabilities as the production environment, but is intended for development and testing purposes. You can use it to try out the Documenso platform and its features before committing to a paid plan. ## How to Use the Demo Environment [#how-to-use-the-demo-environment] {/* prettier-ignore */} ### Navigate to the staging environment [#navigate-to-the-staging-environment] Go to the [staging environment](https://stg-app.documenso.com). ### Create an account [#create-an-account] You need to create a new account for the demo environment. You can't use your production account. ### Pick a paid plan [#pick-a-paid-plan] Choose the appropriate plan for your needs. You can also use the free plan but it's limited to 5 documents per month and up to 10 recipients per document. Whatever plan you choose, you can upgrade later. ### Use a test card [#use-a-test-card] To upgrade to a paid plan, you can use a test card. Example: ``` Card number: 4242 4242 4242 4242 Expiry date: 02/2030 (or any valid future date) CVV: 123 ``` ### Use the platform [#use-the-platform] You can then try out the platform and its features. ### Issues, questions and feedback [#issues-questions-and-feedback] If you have any issues, questions or feedback, please reach out to us on the [Documenso Discord](https://documen.so/discord) or [GitHub](https://github.com/documenso/documenso/issues). # Contributing Translations We are always open for help with translations! Currently we utilise AI to generate the initial translations for new languages, which are then improved over time by our awesome community. If you are looking for development notes on translations, you can find them [here](/docs/developers/local-development/translations). Contributions are made through GitHub Pull Requests, so you will need a GitHub account to contribute. ## Overview [#overview] We store our translations in PO files, which are located in our GitHub repository [here](https://github.com/documenso/documenso/tree/main/packages/lib/translations). The translation files are organized into folders represented by their respective language codes (`en` for English, `de` for German, etc). Each PO file contains translations which look like this: ```po #: apps/remix/app/(signing)/sign/[token]/no-longer-available.tsx:61 msgid "Want to send slick signing links like this one? <0>Check out Documenso." msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso." ``` * `msgid`: The original text in English (never edit this manually) * `msgstr`: The translated text in the target language Notice the `<0>` tags? These represent HTML elements and must remain in both the `msgid` and `msgstr`. Make sure to translate the content between these tags while keeping the tags intact. ## How to Contribute [#how-to-contribute] ### Updating Existing Translations [#updating-existing-translations] {/* prettier-ignore */} Fork the repository Navigate to the appropriate language folder and open the PO file you want to update Make your changes, ensuring you follow the PO file format Commit your changes with a message such as chore: update German translations Create a Pull Request ### Adding a New Language [#adding-a-new-language] If you want to add translations for a language that doesn't exist yet: {/* prettier-ignore */} Create an issue in our GitHub repository requesting the addition of the new language Wait for our team to review and approve the request Once approved, we will set up the necessary files and kickstart the translations with AI to provide initial coverage ## Need Help? [#need-help] If you have any questions, hop into our [Discord](https://documen.so/discord) and ask us directly! Thank you for helping make Documenso more accessible to users around the world! ## See Also [#see-also] * [Translations (Development)](/docs/developers/local-development/translations) - Technical guide to translations in code * [Contributing Guide](/docs/developers/contributing) - General contributing guidelines # Contributing to Documenso ## Overview [#overview] If you plan to contribute to Documenso, please take a moment to feel awesome. People like you are what open source is about. Any contributions, no matter how big or small, are highly appreciated. This guide will help you get started with contributing to Documenso. ## Before Getting Started [#before-getting-started] {/* prettier-ignore */} ### Check the existing issues and pull requests [#check-the-existing-issues-and-pull-requests] Search the existing [issues](https://github.com/documenso/documenso/issues) to see if someone else reported the same issue. Or, check the [existing PRs](https://github.com/documenso/documenso/pulls) to see if someone else is already working on the same thing. ### Creating a new issue [#creating-a-new-issue] If there is no issue or PR for the problem you are facing, feel free to create a new issue. Make sure to provide as much detail as possible, including the steps to reproduce the issue. ### Picking an existing issue [#picking-an-existing-issue] If you pick an existing issue, take into consideration the discussion on the issue. ### Contributor license agreement [#contributor-license-agreement] Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions. ## Taking Issues [#taking-issues] Before taking an issue, ensure that: * The issue has been assigned the public label. * The issue is clearly defined and understood. * No one has been assigned to the issue. * No one has expressed the intention to work on it. After that: 1. Comment on the issue with your intention to work on it. 2. Start working on the issue. Feel free to ask for help, clarification or guidance if needed. We are here to help you. ## Developing [#developing] The development branch is `main`, and all pull requests should be made against this branch. Here's how you can get started with developing: {/* prettier-ignore */} ### Set up Documenso locally [#set-up-documenso-locally] To set up your local environment, check out the [local development](/docs/developers/local-development) guide. ### Pick a task [#pick-a-task] Find an issue to work on or create a new one. > Before working on an issue, ensure that no one else is working on it. If no one is assigned to the issue, you can pick it up by leaving a comment and asking to assign it to you. Before creating a new issue, check the existing issues to see if someone else has already reported it. ### Create a new branch [#create-a-new-branch] After you're assigned an issue, you can start working on it. Create a new branch for your feature or bug fix. When creating a branch, make sure that the branch name: * starts with the correct prefix: `feat/` for new features, `fix/` for bug fixes, etc. * includes the issue ID you are working on (if applicable). * is descriptive. ```sh git checkout -b feat/issue-id-your-branch-name ## Example git checkout -b feat/1234-add-share-button-to-articles ``` In the pull request description, include `references #yyyy` or `fixes #yyyy` to link it to the issue you are working on. ### Implement your changes [#implement-your-changes] Start working on the issue you picked up and implement the changes. Make sure to test your changes locally and ensure that they work as expected. ### Open a pull request [#open-a-pull-request] After implementing your changes, open a pull request against the `main` branch. If you need help getting started, [join us on Discord](https://documen.so/discord). ## Building [#building] Before pushing code or creating pull requests, please ensure you can successfully create a successful production build. You can build the project by running the following command in your terminal: ```bash npm run build ``` Once the project builds successfully, you can push your code changes or create a pull request. Remember to run tests and perform any necessary checks before finalizing your changes. As a result, we can collaborate more effectively and maintain a high standard of code quality in our project. ## See Also [#see-also] * [Local Development](/docs/developers/local-development) - Set up your development environment * [Contributing Translations](/docs/developers/contributing/contributing-translations) - Help translate Documenso # CSS Variables Custom CSS and CSS variables are available on the [Platform Plan](https://documen.so/platform-cta-pricing). ## CSS Variables [#css-variables] Use the `cssVars` prop on any embed component to override default colors, spacing, and more. ```jsx ``` ### Colors [#colors] | Variable | Description | | -------------------------- | ----------------------------------------------------------------- | | `background` | Base background color | | `foreground` | Base text color | | `muted` | Muted/subtle background color | | `mutedForeground` | Muted/subtle text color | | `popover` | Popover/dropdown background color | | `popoverForeground` | Popover/dropdown text color | | `card` | Card background color | | `cardBorder` | Card border color | | `cardBorderTint` | Card border tint/highlight color | | `cardForeground` | Card text color | | `fieldCard` | Field card background color | | `fieldCardBorder` | Field card border color | | `fieldCardForeground` | Field card text color | | `widget` | Widget background color | | `widgetForeground` | Widget text color | | `border` | Default border color | | `input` | Input field border color | | `primary` | Primary action/button color | | `primaryForeground` | Primary action/button text color | | `secondary` | Secondary action/button color | | `secondaryForeground` | Secondary button text color | | `accent` | Accent/highlight color | | `accentForeground` | Accent/highlight text color | | `destructive` | Destructive/danger action color | | `destructiveForeground` | Destructive/danger text color | | `ring` | Focus ring color | | `warning` | Warning/alert color | | `envelopeEditorBackground` | Envelope editor background color. *V2 Envelope Editor only.* | ### Spacing [#spacing] | Variable | Description | | -------- | ---------------------------------- | | `radius` | Border radius size (e.g. `0.5rem`) | ### Framework Usage [#framework-usage] Pass `cssVars` to any embed component. The syntax varies by framework: ```jsx // React / Preact // Vue // Svelte // Solid ``` ### Color Formats [#color-formats] Colors can be specified in any valid CSS format: * Hex: `#ff0000` * RGB: `rgb(255, 0, 0)` * HSL: `hsl(0, 100%, 50%)` * Named: `red` *** ## Custom CSS [#custom-css] Use the `css` prop to inject a CSS string for more targeted control: ```jsx ``` *** ## CSS Class Targets [#css-class-targets] Specific parts of the embed can be targeted with CSS classes for granular styling. ### Component Classes [#component-classes] | Class | Description | | --------------------------------- | --------------------------------------------- | | `.embed--Root` | Main container for the embedded experience | | `.embed--DocumentContainer` | Container for the document and signing widget | | `.embed--DocumentViewer` | Container for the document viewer | | `.embed--DocumentWidget` | The signing widget container | | `.embed--DocumentWidgetContainer` | Outer container for the signing widget | | `.embed--DocumentWidgetHeader` | Header section of the signing widget | | `.embed--DocumentWidgetContent` | Main content area of the signing widget | | `.embed--DocumentWidgetForm` | Form section within the signing widget | | `.embed--DocumentWidgetFooter` | Footer section of the signing widget | | `.embed--WaitingForTurn` | Waiting screen when it is not the user's turn | | `.embed--DocumentCompleted` | Completion screen after signing | | `.field--FieldRootContainer` | Base container for document fields | ### Field Data Attributes [#field-data-attributes] Fields expose data attributes for state-based styling: | Attribute | Values | Description | | ------------------- | ---------------------------------------------- | ------------------------------------ | | `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field | | `[data-inserted]` | `true`, `false` | Whether the field has been filled | | `[data-validate]` | `true`, `false` | Whether the field is being validated | ### Example [#example] ```css /* Style signature fields */ .field--FieldRootContainer[data-field-type='SIGNATURE'] { background-color: rgba(0, 0, 0, 0.02); } /* Style filled fields */ .field--FieldRootContainer[data-inserted='true'] { background-color: var(--primary); opacity: 0.2; } /* Custom widget styling */ .embed--DocumentWidget { background-color: #ffffff; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } ``` ### Additional Examples [#additional-examples] ```css /* Style all field containers with transitions */ .field--FieldRootContainer { transition: all 200ms ease; } /* Custom styles for the waiting screen */ .embed--WaitingForTurn { background-color: #f9fafb; padding: 2rem; } /* Responsive adjustments for the document container */ @media (min-width: 768px) { .embed--DocumentContainer { gap: 2rem; } } ``` *** ## Best Practices [#best-practices] Ensure sufficient contrast between background and foreground colors for accessibility. If dark mode is not disabled, verify your variables work in both modes. Align `primary` and `accent` colors with your brand for a cohesive look. Use a border radius that matches your application's design system. *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [React](/docs/developers/embedding/sdks/react) - React SDK usage * [Vue](/docs/developers/embedding/sdks/vue) - Vue SDK usage # Direct Links ## What Are Direct Links? [#what-are-direct-links] Direct links are unique URLs tied to a template that allow anyone to: {/* prettier-ignore */} View and sign a document without receiving an email invitation Enter their own name and email address Complete signature fields and submit the document When someone uses a direct link, Documenso creates a new document from the template with that person as the signer. ### When to Use Direct Links [#when-to-use-direct-links] * Collecting signatures from unknown recipients (forms, waivers, petitions) * Embedding signing in your website or application * Self-service contracts where customers initiate signing * Public-facing agreements that anyone can sign ### Limitations [#limitations] * Only work with templates, not individual documents * Each link is tied to one recipient role in the template * Recipients enter their own information (you cannot prefill recipient details) ## Creating Direct Link Templates [#creating-direct-link-templates] Before embedding, you need a template with direct links enabled. ### Via the Dashboard [#via-the-dashboard] {/* prettier-ignore */} Go to **Templates** and create or select a template Team templates Click the three-dot menu and select **Direct link** Click **Enable direct link signing** Choose which recipient in your template will use the direct link Enable direct link Copy the generated URL ### Via the API [#via-the-api] Create a direct link for an existing template: ```bash # Create direct link for template curl -X POST "https://app.documenso.com/api/v2/template/direct/create" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "templateId": 123, "directRecipientId": 1 }' ``` ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; const response = await fetch(`${BASE_URL}/template/direct/create`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId: 123, directRecipientId: 1, // Optional: specific recipient to use }), }); const directLink = await response.json(); console.log('Direct link token:', directLink.token); // URL: https://app.documenso.com/d/{token} ``` ### Response [#response] ```json { "id": 456, "token": "abc123xyz", "templateId": 123, "directTemplateRecipientId": 1, "enabled": true, "createdAt": "2025-01-15T10:00:00.000Z" } ``` The direct link URL format is: ``` https://app.documenso.com/d/{token} ``` Copy recipient token *** ## Embedding in an iframe [#embedding-in-an-iframe] Embed the signing experience directly in your application using an iframe. ### Basic iframe Embedding [#basic-iframe-embedding] ```html ``` ### Responsive iframe [#responsive-iframe] ```html
``` ### React Component Example [#react-component-example] ```tsx function DocumentSigner({ token }: { token: string }) { return (
``` The URL you embed depends on the embed mode you’re using (for example direct links vs sign-token embeds). Use the embed URL provided by Documenso for your flow. ### iframe Customization [#iframe-customization] You can customize the embedded signing experience by passing **encoded options in the iframe URL fragment** (everything after `#`). Documenso expects the fragment to be **base64** of: * `encodeURIComponent(JSON.stringify(options))` #### Supported options [#supported-options] | Option | Type | Description | | ------------------------ | --------- | ---------------------------------------------------------- | | `name` | `string` | Prefill signer name. | | `email` | `string` | Prefill signer email. | | `lockName` | `boolean` | Lock the name field (prevents editing). | | `lockEmail` | `boolean` | Lock the email field (prevents editing). | | `language` | `string` | Force the embed language (e.g. `en`). | | `darkModeDisabled` | `boolean` | Disable dark mode behavior. | | `allowDocumentRejection` | `boolean` | Allow or disallow document rejection. | | `css` | `string` | Inject custom CSS into the embed. | | `cssVars` | `object` | Override embed CSS variables (see the CSS Variables page). | #### Example [#example] ```ts const buildEmbedSrc = (host: string, token: string) => { const options = { name: 'Ada Lovelace', email: 'ada@example.com', lockName: true, lockEmail: true, language: 'en', darkModeDisabled: false, allowDocumentRejection: true, css: ':root { --radius: 12px; }', cssVars: {}, }; const encodedOptions = btoa(encodeURIComponent(JSON.stringify(options))); return `${new URL(`/embed/sign/${token}`, host).toString()}#${encodedOptions}`; }; ``` A complete example can be found in the [Embeds repository](https://github.com/documenso/embeds/blob/main/packages/mitosis/src/sign-document.lite.tsx). The fragment is **not sent to the server** as part of the HTTP request, but it is available to the embedded app in the browser. This makes it a convenient way to pass client-side configuration without changing the base embed URL. # Embedding ## Embedded Signing vs Embedded Editor [#embedded-signing-vs-embedded-editor] Documenso offers two types of embedding: * **Embedded Signing** lets you embed the signing experience in your application. Your users sign documents without leaving your site. Available on Teams Plan and above. * **Embedded Editor** lets you embed document and template *creation and editing* in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Editor](/docs/developers/embedding/editor) guide. This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Editor](/docs/developers/embedding/editor). *** ## Availability [#availability] Embedding is available on **Teams Plan** and above, as well as for **Early Adopters** within a team (Early Adopters can create a team for free). The [Platform Plan](https://documen.so/platform-cta-pricing) adds enhanced customization: * Custom CSS and styling variables * Dark mode controls * Removal of Documenso branding *** ## How It Works [#how-it-works] There are two ways to embed signing, each using a different component and token type. ### Direct Templates [#direct-templates] Direct templates are evergreen - each time a user completes signing, a new document is created from the template. This is the recommended approach for most use cases. Use the `EmbedDirectTemplate` component with a template token: ```jsx import { EmbedDirectTemplate } from '@documenso/embed-react'; { console.log('Signed:', data.documentId); }} />; ``` ### Signing Tokens [#signing-tokens] For advanced integrations where you create documents via the API, you can embed the signing experience for a specific recipient using their signing token. Use the `EmbedSignDocument` component with the recipient's token: ```jsx import { EmbedSignDocument } from '@documenso/embed-react'; { console.log('Signed:', data.documentId); }} />; ``` For most use cases, direct templates are the way to go. Use signing tokens when you need programmatic control over document creation via the API. *** ## Getting Your Token [#getting-your-token] ### Direct Template Token [#direct-template-token] {/* prettier-ignore */} Navigate to your team's templates in Documenso Team Templates Click on a direct link template to copy its URL. The token is the last segment of the URL. For example, `https://app.documenso.com/d/-WoSwWVT-fYOERS2MI37k` has the token `-WoSwWVT-fYOERS2MI37k`. If your template is not a direct link template yet, select **Direct Link** from the three-dot menu on the templates table to enable it. Enable Direct Link Template ### Signing Token [#signing-token] Signing tokens are returned in API responses when distributing a document. You can also get one manually by hovering over a recipient's avatar on a document you own and clicking their email. Copy Recipient Token *** ## Framework SDKs [#framework-sdks] Pick your framework to get started: A [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) SDK (`@documenso/embed-webcomponent`) is also available for use outside of JavaScript frameworks. It works in any environment that supports custom elements. If you prefer not to use any SDK, you can embed signing using [Direct Links](/docs/developers/embedding/direct-links) with a plain iframe or redirect. *** ## Props [#props] ### EmbedDirectTemplate [#embeddirecttemplate] | Prop | Type | Description | | --------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `token` | `string` | **Required.** The direct template token. | | `host` | `string` | Documenso instance URL. Defaults to `https://app.documenso.com`. | | `name` | `string` | Pre-fill the signer's name. | | `lockName` | `boolean` | Prevent the signer from changing their name. | | `email` | `string` | Pre-fill the signer's email. | | `lockEmail` | `boolean` | Prevent the signer from changing their email. | | `externalId` | `string` | Your reference ID, stored with the created document. | | `css` | `string` | Custom CSS string (Platform Plan). | | `cssVars` | `object` | CSS variable overrides for theming (Platform Plan). | | `darkModeDisabled` | `boolean` | Disable dark mode in the embed (Platform Plan). | | `language` | `string` | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts). | | `onDocumentReady` | `function` | Called when the document is loaded and ready. | | `onDocumentCompleted` | `function` | Called when signing is completed. | | `onDocumentError` | `function` | Called when an error occurs. | | `onFieldSigned` | `function` | Called when a field is signed. | | `onFieldUnsigned` | `function` | Called when a field value is cleared. | ### EmbedSignDocument [#embedsigndocument] | Prop | Type | Description | | --------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `token` | `string` | **Required.** The recipient's signing token. | | `host` | `string` | Documenso instance URL. Defaults to `https://app.documenso.com`. | | `name` | `string` | Pre-fill the signer's name. | | `lockName` | `boolean` | Prevent the signer from changing their name. | | `language` | `string` | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts). | | `onDocumentReady` | `function` | Called when the document is loaded and ready. | | `onDocumentCompleted` | `function` | Called when signing is completed. | | `onDocumentError` | `function` | Called when an error occurs. | *** ## Event Callbacks [#event-callbacks] ### onDocumentCompleted [#ondocumentcompleted] Receives an object with: | Field | Type | Description | | ------------- | -------- | ------------------------------ | | `token` | `string` | The token used for signing. | | `documentId` | `number` | The ID of the signed document. | | `recipientId` | `number` | The ID of the recipient. | ### onFieldSigned [#onfieldsigned] Receives an object with: | Field | Type | Description | | ---------- | --------- | -------------------------------------------- | | `fieldId` | `number` | The ID of the field. | | `value` | `string` | The field value. | | `isBase64` | `boolean` | Whether the value is a base64 encoded image. | ### onFieldUnsigned [#onfieldunsigned] Receives an object with: | Field | Type | Description | | --------- | -------- | -------------------- | | `fieldId` | `number` | The ID of the field. | *** ## More [#more] *** ## See Also [#see-also] * [Documents API](/docs/developers/api/documents) - Create documents programmatically * [Templates API](/docs/developers/api/templates) - Manage templates via API * [Webhooks](/docs/developers/webhooks) - Receive server-side signing notifications # Common Workflows ## Workflow 1: Send a Document for Signature [#workflow-1-send-a-document-for-signature] The most common workflow: upload a PDF, add recipients with signature fields, and send for signing. {/* prettier-ignore */} Build the recipient payload with signature and date fields Create the envelope via POST /envelope/create with the PDF and payload Distribute the document via POST /envelope/distribute to send signing links ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; type Recipient = { email: string; name: string; role: 'SIGNER' | 'APPROVER' | 'CC' | 'VIEWER'; }; type CreateAndSendResult = { envelopeId: string; recipients: Array<{ email: string; signingUrl: string; }>; }; async function createAndSendDocument( pdfBuffer: Buffer, filename: string, title: string, externalId: string, recipients: Recipient[], ): Promise { const recipientPayload = recipients.map((recipient, index) => ({ email: recipient.email, name: recipient.name, role: recipient.role, signingOrder: index + 1, fields: recipient.role === 'SIGNER' ? [ { identifier: 0, type: 'SIGNATURE', page: 1, positionX: 10, positionY: 80 - index * 10, width: 30, height: 5, }, { identifier: 0, type: 'DATE', page: 1, positionX: 50, positionY: 80 - index * 10, width: 20, height: 3, }, ] : [], })); const formData = new FormData(); formData.append( 'payload', JSON.stringify({ type: 'DOCUMENT', title, externalId, recipients: recipientPayload, meta: { subject: `Please sign: ${title}`, message: 'Please review and sign the attached document.', }, }), ); formData.append('files', new Blob([pdfBuffer], { type: 'application/pdf' }), filename); const createResponse = await fetch(`${BASE_URL}/envelope/create`, { method: 'POST', headers: { Authorization: API_TOKEN }, body: formData, }); if (!createResponse.ok) { const error = await createResponse.json(); throw new Error(`Failed to create envelope: ${error.message}`); } const { id: envelopeId } = 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 }), }); if (!distributeResponse.ok) { const error = await distributeResponse.json(); throw new Error(`Failed to send document: ${error.message}`); } const distributeResult = await distributeResponse.json(); return { envelopeId, recipients: distributeResult.recipients.map( (r: { email: string; signingUrl: string }) => ({ email: r.email, signingUrl: r.signingUrl, }), ), }; } const fs = require('fs'); const pdfBuffer = fs.readFileSync('./contract.pdf'); const result = await createAndSendDocument( pdfBuffer, 'contract.pdf', 'Service Agreement', 'nda-contract-ndac214', [ { email: 'client@example.com', name: 'John Smith', role: 'SIGNER' }, { email: 'manager@company.com', name: 'Jane Doe', role: 'SIGNER' }, { email: 'legal@company.com', name: 'Legal Team', role: 'CC' }, ], ); console.log('Document sent:', result.envelopeId); result.recipients.forEach((r) => console.log(`${r.email}: ${r.signingUrl}`)); ``` ```bash #!/bin/bash set -e API_TOKEN="YOUR_API_TOKEN" BASE_URL="https://app.documenso.com/api/v2" PDF_FILE="./contract.pdf" # Step 1: Create envelope with recipients and fields echo "Creating envelope..." ENVELOPE_RESPONSE=$(curl -s -X POST "${BASE_URL}/envelope/create" \ -H "Authorization: ${API_TOKEN}" \ -F 'payload={ "type": "DOCUMENT", "title": "Service Agreement", "externalId": "nda-contract-ndac214", "recipients": [ { "email": "client@example.com", "name": "John Smith", "role": "SIGNER", "signingOrder": 1, "fields": [ { "identifier": 0, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5 }, { "identifier": 0, "type": "DATE", "page": 1, "positionX": 50, "positionY": 80, "width": 20, "height": 3 } ] }, { "email": "manager@company.com", "name": "Jane Doe", "role": "SIGNER", "signingOrder": 2, "fields": [ { "identifier": 0, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 70, "width": 30, "height": 5 } ] } ], "meta": { "subject": "Please sign: Service Agreement", "message": "Please review and sign the attached document." } }' \ -F "files=@${PDF_FILE};type=application/pdf") ENVELOPE_ID=$(echo $ENVELOPE_RESPONSE | jq -r '.id') echo "Created envelope: ${ENVELOPE_ID}" # Step 2: Distribute (send) the document echo "Sending document..." 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 sent!" echo $DISTRIBUTE_RESPONSE | jq '.recipients[] | {email, signingUrl}' ``` `externalId` is your application's own reference for this document, such as an invoice number or a database key. Documenso stores it on the envelope and repeats it in every webhook as `payload.externalId`, so your handler can match the event to your record without keeping a lookup table of Documenso IDs. To react when everyone has signed, see [Workflow 4](#workflow-4-wait-for-completion-with-webhooks). To fetch the finished PDF, see [Workflow 5](#workflow-5-download-signed-documents). *** ## Workflow 2: Create Document from Template with Custom Data [#workflow-2-create-document-from-template-with-custom-data] Use templates for repeatable document workflows. This example creates an employment contract with prefilled data. {/* prettier-ignore */} Fetch the template to get recipient and field IDs Map template fields by label and build a prefillFields array Call POST /template/use with recipients, prefill data, and{' '} distributeDocument: true ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; type EmployeeData = { email: string; name: string; position: string; salary: number; startDate: string; // ISO 8601 format: YYYY-MM-DD department: string; }; type TemplateField = { id: number; type: string; fieldMeta?: { label?: string }; }; type TemplateRecipient = { id: number; role: string; signingOrder: number; }; async function sendEmploymentContract( templateId: number, employee: EmployeeData, ): Promise<{ documentId: string; signingUrl: string }> { const templateResponse = await fetch(`${BASE_URL}/template/${templateId}`, { headers: { Authorization: API_TOKEN }, }); if (!templateResponse.ok) { throw new Error('Template not found'); } const template = await templateResponse.json(); const employeeRecipient = template.recipients.find( (r: TemplateRecipient) => r.role === 'SIGNER' && r.signingOrder === 1, ); if (!employeeRecipient) { throw new Error('Template missing employee recipient'); } const fieldsByLabel = new Map(); template.fields.forEach((field: TemplateField) => { if (field.fieldMeta?.label) { fieldsByLabel.set(field.fieldMeta.label.toLowerCase(), field); } }); const prefillFields = []; const positionField = fieldsByLabel.get('position') || fieldsByLabel.get('job title'); if (positionField) { prefillFields.push({ id: positionField.id, type: 'text', value: employee.position }); } const salaryField = fieldsByLabel.get('salary') || fieldsByLabel.get('compensation'); if (salaryField) { prefillFields.push({ id: salaryField.id, type: 'number', value: employee.salary.toString(), }); } const startDateField = fieldsByLabel.get('start date'); if (startDateField) { prefillFields.push({ id: startDateField.id, type: 'date', value: employee.startDate, }); } const departmentField = fieldsByLabel.get('department'); if (departmentField) { prefillFields.push({ id: departmentField.id, type: 'dropdown', value: employee.department, }); } const createResponse = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId, recipients: [ { id: employeeRecipient.id, email: employee.email, name: employee.name }, ], prefillFields, override: { title: `Employment Contract - ${employee.name}`, 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]}`, }), }); if (!createResponse.ok) { const error = await createResponse.json(); throw new Error(`Failed to create document: ${error.message}`); } const document = await createResponse.json(); return { documentId: document.id, signingUrl: document.recipients[0].signingUrl, }; } const result = await sendEmploymentContract(123, { email: 'alice.johnson@example.com', name: 'Alice Johnson', position: 'Senior Engineer', salary: 120000, startDate: '2025-03-01', department: 'Engineering', }); console.log('Contract sent:', result.documentId); console.log('Signing URL:', result.signingUrl); ``` ```bash #!/bin/bash set -e API_TOKEN="YOUR_API_TOKEN" BASE_URL="https://app.documenso.com/api/v2" TEMPLATE_ID=123 # Variables EMPLOYEE_EMAIL="alice.johnson@example.com" EMPLOYEE_NAME="Alice Johnson" POSITION="Senior Engineer" SALARY="120000" START_DATE="2025-03-01" DEPARTMENT="Engineering" # Fetch template to get recipient ID TEMPLATE=$(curl -s -X GET "${BASE_URL}/template/${TEMPLATE_ID}" \ -H "Authorization: ${API_TOKEN}") # Extract first signer recipient ID RECIPIENT_ID=$(echo $TEMPLATE | jq '.recipients[] | select(.role == "SIGNER" and .signingOrder == 1) | .id') # Create document from template with prefilled data RESPONSE=$(curl -s -X POST "${BASE_URL}/template/use" \ -H "Authorization: ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"templateId\": ${TEMPLATE_ID}, \"recipients\": [ { \"id\": ${RECIPIENT_ID}, \"email\": \"${EMPLOYEE_EMAIL}\", \"name\": \"${EMPLOYEE_NAME}\" } ], \"prefillFields\": [ {\"id\": 101, \"type\": \"text\", \"value\": \"${POSITION}\"}, {\"id\": 102, \"type\": \"number\", \"value\": \"${SALARY}\"}, {\"id\": 103, \"type\": \"date\", \"value\": \"${START_DATE}\"}, {\"id\": 104, \"type\": \"dropdown\", \"value\": \"${DEPARTMENT}\"} ], \"override\": { \"title\": \"Employment Contract - ${EMPLOYEE_NAME}\", \"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}' ``` *** ## Workflow 3: Bulk Send Documents [#workflow-3-bulk-send-documents] Send the same document to multiple recipients in parallel. Useful for policy acknowledgments, NDAs, or announcements. {/* prettier-ignore */} Fetch the template and get the signer recipient slot ID For each recipient, call POST /template/use with{' '} distributeDocument: true Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute) ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; type BulkRecipient = { email: string; name: string; }; type BulkSendResult = { successful: Array<{ email: string; envelopeId: string; signingUrl: string }>; failed: Array<{ email: string; error: string }>; }; async function bulkSendFromTemplate( templateId: number, recipients: BulkRecipient[], concurrency = 5, ): Promise { const templateResponse = await fetch(`${BASE_URL}/template/${templateId}`, { headers: { Authorization: API_TOKEN }, }); if (!templateResponse.ok) { throw new Error('Template not found'); } const template = await templateResponse.json(); const signerSlot = template.recipients.find( (r: { role: string }) => r.role === 'SIGNER', ); if (!signerSlot) { throw new Error('Template has no signer recipient'); } const results: BulkSendResult = { successful: [], failed: [] }; for (let i = 0; i < recipients.length; i += concurrency) { const batch = recipients.slice(i, i + concurrency); const batchResults = await Promise.allSettled( batch.map(async (recipient) => { const response = await fetch(`${BASE_URL}/template/use`, { method: 'POST', headers: { Authorization: API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ templateId, recipients: [ { id: signerSlot.id, email: recipient.email, name: recipient.name }, ], distributeDocument: true, externalId: `bulk-${Date.now()}-${recipient.email}`, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Unknown error'); } const document = await response.json(); return { email: recipient.email, envelopeId: document.id, signingUrl: document.recipients[0].signingUrl, }; }), ); batchResults.forEach((result, index) => { if (result.status === 'fulfilled') { results.successful.push(result.value); } else { results.failed.push({ email: batch[index].email, error: result.reason.message, }); } }); if (i + concurrency < recipients.length) { await new Promise((resolve) => setTimeout(resolve, 1000)); } } return results; } const recipients = [ { email: 'employee1@company.com', name: 'Employee One' }, { email: 'employee2@company.com', name: 'Employee Two' }, { email: 'employee3@company.com', name: 'Employee Three' }, ]; const results = await bulkSendFromTemplate(456, recipients); console.log(`Sent: ${results.successful.length}`); console.log(`Failed: ${results.failed.length}`); results.failed.forEach((f) => console.error(`${f.email}: ${f.error}`)); ``` ```bash #!/bin/bash API_TOKEN="YOUR_API_TOKEN" BASE_URL="https://app.documenso.com/api/v2" TEMPLATE_ID=456 # Get signer slot ID from template SIGNER_ID=$(curl -s -X GET "${BASE_URL}/template/${TEMPLATE_ID}" \ -H "Authorization: ${API_TOKEN}" | jq '.recipients[] | select(.role == "SIGNER") | .id') # Recipients to process RECIPIENTS=( "employee1@company.com:Employee One" "employee2@company.com:Employee Two" "employee3@company.com:Employee Three" ) # Send to each recipient for RECIPIENT in "${RECIPIENTS[@]}"; do EMAIL=$(echo $RECIPIENT | cut -d: -f1) NAME=$(echo $RECIPIENT | cut -d: -f2) echo "Sending to ${EMAIL}..." RESPONSE=$(curl -s -X POST "${BASE_URL}/template/use" \ -H "Authorization: ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"templateId\": ${TEMPLATE_ID}, \"recipients\": [{ \"id\": ${SIGNER_ID}, \"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')" else echo " Failed: $(echo $RESPONSE | jq -r '.message')" fi # Rate limiting delay sleep 0.5 done ``` The API allows 1000 requests per minute (your organisation may have its own lower limit). For large batches, implement rate limiting with delays between requests to avoid hitting limits. *** ## Workflow 4: Wait for Completion with Webhooks [#workflow-4-wait-for-completion-with-webhooks] Set up webhooks to receive real-time notifications when documents are signed or completed. {/* prettier-ignore */} **Create a webhook endpoint**: Implement an HTTP handler that verifies the signature and processes events **Register the webhook**: Add your endpoint URL and secret in Team Settings → Webhooks **Optional: poll as fallback**: For critical workflows, poll envelope status if webhooks are unreliable ### Create a Webhook Endpoint [#create-a-webhook-endpoint] ```typescript // Express.js webhook handler import express from 'express'; const app = express(); app.use(express.json()); const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET; type WebhookPayload = { event: string; payload: { id: number; externalId: string | null; status: string; title: string; completedAt: string | null; Recipient: Array<{ id: number; email: string; name: string; signingStatus: string; signedAt: string | null; rejectionReason: string | null; }>; }; createdAt: string; }; app.post('/webhooks/documenso', (req, res) => { // Verify webhook signature const secret = req.headers['x-documenso-secret'] as string; if (secret !== WEBHOOK_SECRET) { console.error('Invalid webhook secret'); return res.status(401).send('Unauthorized'); } const { event, payload } = req.body as WebhookPayload; switch (event) { case 'DOCUMENT_SENT': console.log(`Document ${payload.id} sent to recipients`); // Update your database, notify users, etc. break; case 'DOCUMENT_OPENED': const opener = payload.Recipient.find((r) => r.signingStatus === 'NOT_SIGNED'); console.log(`${opener?.name} opened document ${payload.id}`); break; case 'DOCUMENT_SIGNED': const signer = payload.Recipient.find((r) => r.signingStatus === 'SIGNED' && r.signedAt); console.log(`${signer?.name} signed document ${payload.id}`); // Trigger next steps in your workflow break; case 'DOCUMENT_COMPLETED': console.log(`Document ${payload.id} completed at ${payload.completedAt}`); // All signatures collected - trigger fulfillment handleDocumentCompleted(payload); break; case 'DOCUMENT_REJECTED': const rejecter = payload.Recipient.find((r) => r.signingStatus === 'REJECTED'); console.log(`${rejecter?.name} rejected: ${rejecter?.rejectionReason}`); // Handle rejection - notify admin, restart process, etc. break; } // Always respond 200 quickly res.status(200).send('OK'); }); async function handleDocumentCompleted(payload: WebhookPayload['payload']) { // Example: Update order status, send confirmation, download PDF console.log(`Processing completed document: ${payload.externalId || payload.id}`); // Your business logic here } app.listen(3000, () => console.log('Webhook server running on port 3000')); ``` ### Register the Webhook [#register-the-webhook] Register your endpoint in the Documenso dashboard: {/* prettier-ignore */} Go to **Team Settings** → **Webhooks** Click **Create Webhook** Enter your webhook URL (e.g. [https://your-app.com/webhooks/documenso](https://your-app.com/webhooks/documenso)) Select which events to subscribe to: DOCUMENT\_SENT, DOCUMENT\_OPENED,{' '} DOCUMENT\_SIGNED, DOCUMENT\_COMPLETED, DOCUMENT\_REJECTED Enter your webhook secret (the same value stored in DOCUMENSO\_WEBHOOK\_SECRET) Click **Create Webhook** See [Webhook Setup](/docs/developers/webhooks/setup) for detailed instructions. ### Poll as Fallback [#poll-as-fallback] For critical workflows, implement polling as a fallback in case webhooks fail: ```typescript async function pollForCompletion( envelopeId: string, timeoutMs = 86400000, // 24 hours intervalMs = 60000, // 1 minute ): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const response = await fetch(`${BASE_URL}/envelope/${envelopeId}`, { headers: { Authorization: API_TOKEN }, }); const envelope = await response.json(); if (envelope.status === 'COMPLETED') { return true; } if (envelope.status === 'REJECTED') { throw new Error('Document was rejected'); } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } return false; } ``` *** ## Workflow 5: Download Signed Documents [#workflow-5-download-signed-documents] After a document is completed, download the signed PDF with all signatures embedded. {/* prettier-ignore */} Get the envelope with GET /envelope/:envelopeId and confirm status is{' '} COMPLETED Use envelopeItems\[0].id and call{' '} GET /envelope/item/:itemId/download?version=signed ```typescript const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; type DownloadVersion = 'signed' | 'original'; async function downloadDocument( envelopeId: string, version: DownloadVersion = 'signed', ): Promise<{ buffer: Buffer; filename: string }> { const envelopeResponse = await fetch(`${BASE_URL}/envelope/${envelopeId}`, { headers: { Authorization: API_TOKEN }, }); if (!envelopeResponse.ok) { throw new Error('Envelope not found'); } const envelope = await envelopeResponse.json(); if (version === 'signed' && envelope.status !== 'COMPLETED') { throw new Error('Document must be completed to download signed version'); } const envelopeItem = envelope.envelopeItems[0]; if (!envelopeItem) { throw new Error('No document found in envelope'); } const downloadResponse = await fetch( `${BASE_URL}/envelope/item/${envelopeItem.id}/download?version=${version}`, { headers: { Authorization: API_TOKEN } }, ); if (!downloadResponse.ok) { const error = await downloadResponse.json(); throw new Error(`Download failed: ${error.message}`); } const buffer = Buffer.from(await downloadResponse.arrayBuffer()); const filename = `${envelope.title.replace(/[^a-z0-9]/gi, '_')}_${version}.pdf`; return { buffer, filename }; } async function downloadAllCompletedDocuments(outputDir: string): Promise { const fs = require('fs'); const path = require('path'); let page = 1; let hasMore = true; while (hasMore) { const response = await fetch( `${BASE_URL}/envelope?status=COMPLETED&type=DOCUMENT&page=${page}&perPage=50`, { headers: { Authorization: API_TOKEN } }, ); const { data, pagination } = await response.json(); for (const envelope of data) { try { const { buffer, filename } = await downloadDocument(envelope.id, 'signed'); const filepath = path.join(outputDir, filename); fs.writeFileSync(filepath, buffer); console.log(`Downloaded: ${filename}`); } catch (error) { console.error(`Failed to download ${envelope.id}:`, error); } await new Promise((resolve) => setTimeout(resolve, 500)); } hasMore = page < pagination.totalPages; page++; } } const { buffer, filename } = await downloadDocument('envelope_abc123', 'signed'); require('fs').writeFileSync(`./downloads/${filename}`, buffer); await downloadAllCompletedDocuments('./downloads'); ``` ```bash #!/bin/bash API_TOKEN="YOUR_API_TOKEN" BASE_URL="https://app.documenso.com/api/v2" ENVELOPE_ID="envelope_abc123" OUTPUT_DIR="./downloads" mkdir -p $OUTPUT_DIR # Get envelope details ENVELOPE=$(curl -s -X GET "${BASE_URL}/envelope/${ENVELOPE_ID}" \ -H "Authorization: ${API_TOKEN}") STATUS=$(echo $ENVELOPE | jq -r '.status') TITLE=$(echo $ENVELOPE | jq -r '.title') ITEM_ID=$(echo $ENVELOPE | jq -r '.envelopeItems[0].id') # Check if completed if [ "$STATUS" != "COMPLETED" ]; then echo "Error: Document status is ${STATUS}, must be COMPLETED to download signed version" exit 1 fi # Download signed document FILENAME="${TITLE// /_}_signed.pdf" curl -s -X GET "${BASE_URL}/envelope/item/${ITEM_ID}/download?version=signed" \ -H "Authorization: ${API_TOKEN}" \ -o "${OUTPUT_DIR}/${FILENAME}" echo "Downloaded: ${OUTPUT_DIR}/${FILENAME}" # Download original (optional) ORIG_FILENAME="${TITLE// /_}_original.pdf" curl -s -X GET "${BASE_URL}/envelope/item/${ITEM_ID}/download?version=original" \ -H "Authorization: ${API_TOKEN}" \ -o "${OUTPUT_DIR}/${ORIG_FILENAME}" echo "Downloaded: ${OUTPUT_DIR}/${ORIG_FILENAME}" ``` The signed PDF is only available after all recipients have completed signing (document status is `COMPLETED`). Attempting to download before completion returns the original document without signatures. *** ## Error Handling Patterns [#error-handling-patterns] Implement robust error handling for production integrations. Retry on 429 (rate limit) and 5xx errors with exponential backoff; respect Retry-After when present. ```typescript type RetryOptions = { maxRetries?: number; baseDelayMs?: number; maxDelayMs?: number; }; async function fetchWithRetry( url: string, options: RequestInit, retryOptions: RetryOptions = {}, ): Promise { const { maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 30000 } = retryOptions; let lastError: Error | null = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); // Retry on rate limit if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); // Honor Retry-After exactly; the cap only applies to the exponential fallback. const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs); console.log(`Rate limited, waiting ${delay}ms...`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } // Retry on server errors if (response.status >= 500) { const delay = baseDelayMs * Math.pow(2, attempt); console.log(`Server error ${response.status}, retrying in ${delay}ms...`); await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs))); continue; } return response; } catch (error) { lastError = error as Error; const delay = baseDelayMs * Math.pow(2, attempt); console.log(`Network error, retrying in ${delay}ms...`, error); await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs))); } } throw lastError || new Error('Max retries exceeded'); } ``` Parse API errors into a typed class and handle by status code (400, 401, 404, 429, etc.). ```typescript type ApiError = { message: string; code?: string; statusCode: number; details?: Record; }; class DocumensoApiError extends Error { code?: string; statusCode: number; details?: Record; constructor(error: ApiError) { super(error.message); this.name = 'DocumensoApiError'; this.code = error.code; this.statusCode = error.statusCode; this.details = error.details; } } async function handleApiResponse(response: Response): Promise { if (!response.ok) { const error = await response.json(); throw new DocumensoApiError({ message: error.message || 'Unknown error', code: error.code, statusCode: response.status, details: error.details, }); } return response.json(); } // Usage with specific error handling try { const response = await fetch(`${BASE_URL}/envelope/create`, { method: 'POST', headers: { Authorization: API_TOKEN }, body: formData, }); const envelope = await handleApiResponse<{ id: string }>(response); console.log('Created:', envelope.id); } catch (error) { if (error instanceof DocumensoApiError) { switch (error.statusCode) { case 400: console.error('Invalid request:', error.message); // Fix request and retry break; case 401: console.error('Authentication failed - check API token'); break; case 404: console.error('Resource not found:', error.message); break; case 429: console.error('Rate limited - slow down requests'); break; default: console.error('API error:', error.message); } } else { console.error('Network or unknown error:', error); } } ``` Validate recipients (email, name, role) and PDF (magic bytes, size) before calling the API. ```typescript function validateRecipients(recipients: Array<{ email: string; name: string; role: string }>) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const validRoles = ['SIGNER', 'APPROVER', 'CC', 'VIEWER']; const errors: string[] = []; recipients.forEach((recipient, index) => { if (!recipient.email || !emailRegex.test(recipient.email)) { errors.push(`Recipient ${index + 1}: Invalid email address`); } if (!recipient.name || recipient.name.trim().length === 0) { errors.push(`Recipient ${index + 1}: Name is required`); } if (!validRoles.includes(recipient.role)) { errors.push(`Recipient ${index + 1}: Invalid role "${recipient.role}"`); } }); if (errors.length > 0) { throw new Error(`Validation failed:\n${errors.join('\n')}`); } } function validatePdfFile(buffer: Buffer, maxSizeMb = 10) { // Check PDF magic bytes const pdfHeader = buffer.slice(0, 5).toString(); if (pdfHeader !== '%PDF-') { throw new Error('File is not a valid PDF'); } // Check file size const sizeMb = buffer.length / (1024 * 1024); if (sizeMb > maxSizeMb) { throw new Error(`File size ${sizeMb.toFixed(2)}MB exceeds limit of ${maxSizeMb}MB`); } } ``` *** ## See Also [#see-also] * [API Reference](https://openapi.documenso.com/) - Full endpoint documentation * [Documents API](/docs/developers/api/documents) - Document operations * [Templates API](/docs/developers/api/templates) - Template management * [Webhooks](/docs/developers/webhooks/setup) - Webhook configuration * [Webhook Events](/docs/developers/webhooks/events) - Event payload reference # Examples # Authentication ## Prerequisites [#prerequisites] * A Documenso account (cloud or self-hosted) * A Documenso account on any plan (Free, Individual, Team, or Enterprise) Free accounts include API access with a limit of 5 documents per month. [Upgrade to a paid plan](https://documen.so/pricing) for higher limits. ## Create an API Token [#create-an-api-token] {/* prettier-ignore */} ### Open settings [#open-settings] * Log in to your Documenso account * Click your avatar in the top right corner * Select **Settings** from the dropdown menu User dropdown menu ### Navigate to the API Tokens tab [#navigate-to-the-api-tokens-tab] Go to **Settings** and open the **API Tokens** tab. API tokens page ### Generate a new token [#generate-a-new-token] * 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 * Click **Create Token** ### Copy your token [#copy-your-token] Your token is displayed once after creation. Copy it immediately and store it securely. API key display You cannot view the token again after leaving this page. If you lose it, you must create a new token. ## Using Your Token [#using-your-token] Include the token in the `Authorization` header of your HTTP requests. ### cURL [#curl] ```bash curl https://app.documenso.com/api/v2/document \ -H "Authorization: api_xxxxxxxxxxxxxxxx" ``` ### JavaScript / TypeScript [#javascript--typescript] ```typescript const response = await fetch('https://app.documenso.com/api/v2/document', { method: 'GET', headers: { Authorization: 'api_xxxxxxxxxxxxxxxx', }, }); const documents = await response.json(); ``` ### Using the TypeScript SDK [#using-the-typescript-sdk] Documenso provides official SDKs that handle authentication for you: ```typescript import { Documenso } from '@documenso/sdk-typescript'; const client = new Documenso({ apiKey: 'api_xxxxxxxxxxxxxxxx', }); const documents = await client.documents.find(); ``` SDKs are available for [TypeScript](https://github.com/documenso/sdk-typescript), [Python](https://github.com/documenso/sdk-python), and [Go](https://github.com/documenso/sdk-go). ## API Base URLs [#api-base-urls] | Environment | Base URL | | ----------- | -------------------------------------- | | Production | `https://app.documenso.com/api/v2` | | Staging | `https://stg-app.documenso.com/api/v2` | | Self-hosted | `https://your-domain.com/api/v2` | API V1 is deprecated. Use V2 for all new integrations. V1 only works with legacy documents created before the envelope system. If you need V1 documentation for migration purposes, see the [V1 OpenAPI reference](https://app.documenso.com/api/v1/openapi). The API is available on all plans, including Free (5 documents per month). [Fair Use](/docs/policies/fair-use) applies to all API usage. ## Token Security [#token-security] API tokens grant full access to your account. 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. * **Set expiration dates.** Shorter expiration periods reduce risk if a token is compromised. * **Rotate tokens regularly.** Create new tokens and revoke old ones periodically. * **Use separate tokens per integration.** If one is compromised, you only need to revoke that specific token. * **Revoke unused tokens.** Delete tokens you no longer need from the API Tokens settings page. ### Environment Variables [#environment-variables] Store your token in an environment variable rather than hardcoding it: ```bash # .env (do not commit this file) DOCUMENSO_API_KEY=api_xxxxxxxxxxxxxxxx ``` ```typescript const client = new Documenso({ apiKey: process.env.DOCUMENSO_API_KEY, }); ``` ## Token Scope [#token-scope] API tokens have full access to your account, 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. ## Revoking a Token [#revoking-a-token] To revoke a token: {/* prettier-ignore */} Go to **Settings** > **API Tokens** Find the token you want to revoke Click the delete icon next to the token Confirm the deletion Revoked tokens stop working immediately. Any integrations using that token will receive `401 Unauthorized` errors. ## Troubleshooting [#troubleshooting] Check that you included the token in the `Authorization` header. Create a new token in settings. Ensure you're accessing resources owned by the token's account. ## Next Steps [#next-steps] * [Make your first API call](/docs/developers/getting-started/first-api-call) - Create a document via the API * [API Reference](/docs/developers/api) - Explore available endpoints # First API Call ## Prerequisites [#prerequisites] Before starting, you need: * A Documenso account (cloud or self-hosted) * An API token ([create one in team settings](/docs/developers/getting-started/authentication)) * A PDF file to send for signing API tokens have full access to your account. Store them securely and never commit them to version control. ## Limitations [#limitations] The API cannot: * Sign documents on behalf of recipients (recipients must sign themselves) * Convert non-PDF files to PDF (you must upload PDFs) * Retrieve the signed PDF until all recipients have completed signing ## Base URL [#base-url] All API requests use the following base URLs: | Environment | Base URL | | ----------- | -------------------------------------- | | Production | `https://app.documenso.com/api/v2` | | Staging | `https://stg-app.documenso.com/api/v2` | ## Example 1: List Your Documents [#example-1-list-your-documents] Start with a simple GET request to verify your API token works. ```bash curl -X GET "https://app.documenso.com/api/v2/envelope" \ -H "Authorization: YOUR_API_TOKEN" ``` ```javascript const response = await fetch('https://app.documenso.com/api/v2/envelope', { method: 'GET', headers: { 'Authorization': 'YOUR_API_TOKEN', }, }); const data = await response.json(); console.log(data); ``` A successful response returns a list of your documents (envelopes): ```json { "data": [ { "id": "envelope_abc123", "status": "DRAFT", "title": "Contract Agreement", "createdAt": "2025-01-15T10:30:00.000Z" } ], "pagination": { "page": 1, "perPage": 10, "totalPages": 1, "totalItems": 1 } } ``` If you receive a `401 Unauthorized` error, verify your API token is correct and includes the `api_` prefix. ## Example 2: Create a Document with Recipient and Signature Field [#example-2-create-a-document-with-recipient-and-signature-field] The V2 API uses a single endpoint to create a document with recipients and fields in one request. This is the most common pattern for sending documents. ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/create" \ -H "Authorization: YOUR_API_TOKEN" \ -H "Content-Type: multipart/form-data" \ -F 'payload={ "type": "DOCUMENT", "title": "Service Agreement", "recipients": [ { "email": "signer@example.com", "name": "John Smith", "role": "SIGNER", "fields": [ { "identifier": 0, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 5 }, { "identifier": 0, "type": "DATE", "page": 1, "positionX": 50, "positionY": 80, "width": 20, "height": 3 } ] } ] }' \ -F "files=@./contract.pdf;type=application/pdf" ``` ```javascript const fs = require('fs'); const FormData = require('form-data'); const form = new FormData(); const payload = { type: 'DOCUMENT', title: 'Service Agreement', recipients: [ { email: 'signer@example.com', name: 'John Smith', role: 'SIGNER', fields: [ { identifier: 0, type: 'SIGNATURE', page: 1, positionX: 10, positionY: 80, width: 30, height: 5, }, { identifier: 0, type: 'DATE', page: 1, positionX: 50, positionY: 80, width: 20, height: 3, }, ], }, ], }; form.append('payload', JSON.stringify(payload)); form.append('files', fs.createReadStream('./contract.pdf'), { contentType: 'application/pdf', }); const response = await fetch('https://app.documenso.com/api/v2/envelope/create', { method: 'POST', headers: { Authorization: 'YOUR_API_TOKEN', }, body: form, }); const data = await response.json(); console.log('Created envelope:', data.id); ``` ### Understanding Field Positioning [#understanding-field-positioning] Field positions use percentage values (0-100) relative to the PDF page dimensions: | Parameter | Description | | ------------ | ---------------------------------------------------------- | | `positionX` | Horizontal position from left edge (0 = left, 100 = right) | | `positionY` | Vertical position from top edge (0 = top, 100 = bottom) | | `width` | Field width as percentage of page width | | `height` | Field height as percentage of page height | | `page` | Page number (1-indexed) | | `identifier` | Index of the file (0 for first file, 1 for second, etc.) | To place a signature near the bottom-left of the page, use `positionX: 10` and `positionY: 80`. ### Recipient Roles [#recipient-roles] | Role | Description | | ---------- | ----------------------------------------- | | `SIGNER` | Must sign the document | | `APPROVER` | Must approve before signers can sign | | `CC` | Receives a copy but doesn't sign | | `VIEWER` | Can view the document but takes no action | See the [recipient roles](/docs/concepts/recipient-roles) page for more information. ## Example 3: Send the Document for Signing [#example-3-send-the-document-for-signing] After creating a document, it's in `DRAFT` status. To send it to recipients, use the distribute endpoint: ```bash curl -X POST "https://app.documenso.com/api/v2/envelope/envelope_abc123/distribute" \ -H "Authorization: YOUR_API_TOKEN" \ -H "Content-Type: application/json" ``` ```javascript const envelopeId = 'envelope_abc123'; const response = await fetch( `https://app.documenso.com/api/v2/envelope/${envelopeId}/distribute`, { method: 'POST', headers: { Authorization: 'YOUR_API_TOKEN', 'Content-Type': 'application/json', }, }, ); const data = await response.json(); console.log('Document sent:', data); ``` After distribution, recipients receive an email with a link to sign the document. The document status changes from `DRAFT` to `PENDING`. ## Full Workflow Example [#full-workflow-example] Here's a complete script that creates and sends a document: ```javascript const fs = require('fs'); const FormData = require('form-data'); const API_TOKEN = process.env.DOCUMENSO_API_TOKEN; const BASE_URL = 'https://app.documenso.com/api/v2'; async function createAndSendDocument(pdfPath, recipientEmail, recipientName) { // Step 1: Create the envelope with recipient and fields const form = new FormData(); const payload = { type: 'DOCUMENT', title: 'Service Agreement', recipients: [ { email: recipientEmail, name: recipientName, role: 'SIGNER', fields: [ { identifier: 0, type: 'SIGNATURE', page: 1, positionX: 10, positionY: 80, width: 30, height: 5, }, { identifier: 0, type: 'NAME', page: 1, positionX: 10, positionY: 75, width: 30, height: 3, }, { identifier: 0, type: 'DATE', page: 1, positionX: 50, positionY: 80, width: 20, height: 3, }, ], }, ], }; form.append('payload', JSON.stringify(payload)); form.append('files', fs.createReadStream(pdfPath), { contentType: 'application/pdf', }); const createResponse = await fetch(`${BASE_URL}/envelope/create`, { method: 'POST', headers: { 'Authorization': API_TOKEN, }, body: form, }); if (!createResponse.ok) { const error = await createResponse.json(); throw new Error(`Failed to create envelope: ${JSON.stringify(error)}`); } const envelope = await createResponse.json(); console.log('Created envelope:', envelope.id); // Step 2: Send the document for signing const distributeResponse = await fetch( `${BASE_URL}/envelope/${envelope.id}/distribute`, { method: 'POST', headers: { 'Authorization': API_TOKEN, 'Content-Type': 'application/json', }, } ); if (!distributeResponse.ok) { const error = await distributeResponse.json(); throw new Error(`Failed to distribute envelope: ${JSON.stringify(error)}`); } console.log('Document sent for signing!'); return envelope.id; } // Usage createAndSendDocument( './contract.pdf', 'signer@example.com', 'John Smith' ).catch(console.error); ``` ```bash #!/bin/bash set -e API_TOKEN="YOUR_API_TOKEN" BASE_URL="https://app.documenso.com/api/v2" PDF_FILE="./contract.pdf" RECIPIENT_EMAIL="signer@example.com" RECIPIENT_NAME="John Smith" # Step 1: Create the envelope with recipient and fields echo "Creating envelope..." ENVELOPE_RESPONSE=$(curl -s -X POST "${BASE_URL}/envelope/create" \ -H "Authorization: ${API_TOKEN}" \ -H "Content-Type: multipart/form-data" \ -F "payload={ \"type\": \"DOCUMENT\", \"title\": \"Service Agreement\", \"recipients\": [ { \"email\": \"${RECIPIENT_EMAIL}\", \"name\": \"${RECIPIENT_NAME}\", \"role\": \"SIGNER\", \"fields\": [ { \"identifier\": 0, \"type\": \"SIGNATURE\", \"page\": 1, \"positionX\": 10, \"positionY\": 80, \"width\": 30, \"height\": 5 }, { \"identifier\": 0, \"type\": \"DATE\", \"page\": 1, \"positionX\": 50, \"positionY\": 80, \"width\": 20, \"height\": 3 } ] } ] }" \ -F "files=@${PDF_FILE};type=application/pdf") ENVELOPE_ID=$(echo $ENVELOPE_RESPONSE | jq -r '.id') echo "Created envelope: ${ENVELOPE_ID}" # Step 2: Send the document for signing echo "Sending document..." curl -s -X POST "${BASE_URL}/envelope/${ENVELOPE_ID}/distribute" \ -H "Authorization: ${API_TOKEN}" \ -H "Content-Type: application/json" echo "Document sent for signing!" ``` ## Error Handling [#error-handling] The API returns standard HTTP status codes and JSON error responses: | Status Code | Meaning | | ----------- | ------------------------------------------- | | `400` | Bad request - check your request payload | | `401` | Unauthorized - invalid or missing API token | | `404` | Not found - resource doesn't exist | | `429` | Rate limited - wait 60 seconds and retry | | `500` | Server error - retry or contact support | ### Error Response Format [#error-response-format] ```json { "error": "Description of what went wrong", "code": "ERROR_CODE", "statusCode": 400 } ``` ### Common Errors [#common-errors] **Invalid file type:** ```json { "error": "Invalid file type. Only PDF files are supported.", "statusCode": 400 } ``` **Missing required field:** ```json { "error": "Recipient email is required", "statusCode": 400 } ``` **Envelope not found:** ```json { "error": "Envelope not found", "statusCode": 404 } ``` ### Handling Rate Limits [#handling-rate-limits] The API allows 1000 requests per minute per IP address. Your organisation may have its own lower rate limits. When rate limited, wait at least 60 seconds before retrying: ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status === 429) { console.log('Rate limited, waiting 60 seconds...'); await new Promise((resolve) => setTimeout(resolve, 60000)); continue; } return response; } throw new Error('Max retries exceeded'); } ``` ## Next Steps [#next-steps] * [API Reference](https://openapi.documenso.com/) - Full endpoint documentation with request/response schemas * [Webhooks](/docs/developers/webhooks/setup) - Get notified when documents are signed * [Templates](/docs/developers/api/templates) - Create reusable document templates * [SDKs](#official-sdks) - Use typed client libraries ### Official SDKs [#official-sdks] For production applications, consider using the official SDKs: * [TypeScript SDK](https://github.com/documenso/sdk-typescript) * [Python SDK](https://github.com/documenso/sdk-python) * [Go SDK](https://github.com/documenso/sdk-go) # Getting Started # Run in Gitpod Click below to launch a ready-to-use Gitpod workspace in your browser. [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso) ## See Also [#see-also] * [Developer Quickstart](/docs/developers/local-development/quickstart) - Local Docker-based setup * [Manual Setup](/docs/developers/local-development/manual) - Manual setup without Docker # Local Development ## Overview [#overview] There are multiple ways of setting up Documenso for local development. At the moment of writing this documentation, there are 3 ways of running Documenso locally: * [Using the developer quickstart with Docker](/docs/developers/local-development/quickstart) * [Manually setting up the development environment](/docs/developers/local-development/manual) * [Using Gitpod](/docs/developers/local-development/gitpod) Pick the one that fits your needs the best. ## Tech Stack [#tech-stack] * [TypeScript](https://www.typescriptlang.org/) - Language * [React Router v7](https://reactrouter.com/) - Framework * [Hono](https://hono.dev/) - Server * [Prisma](https://www.prisma.io/) - ORM * [Tailwind CSS](https://tailwindcss.com/) - CSS * [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library * [react-email](https://react.email/) - Email Templates * [Lingui](https://lingui.dev/) - Internationalization * [tRPC](https://trpc.io/) - API * [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signing and Manipulation * [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs * [Stripe](https://stripe.com/) - Payments ## See Also [#see-also] * [Contributing](/docs/developers/contributing) - Learn how to contribute to Documenso * [Self-Hosting](/docs/self-hosting) - Deploy your own instance # Manual Setup # Manual Setup [#manual-setup] Follow these steps to set up Documenso on your local machine: {/* prettier-ignore */} ### Fork Documenso [#fork-documenso] Fork the [Documenso repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account. ### Clone repository [#clone-repository] After forking the repository, clone it to your local device by using the following command: ```bash git clone https://github.com//documenso ``` ### Install dependencies [#install-dependencies] Run `npm i` in the root directory to install the dependencies required for the project. ### Set up environment variables [#set-up-environment-variables] Set up the following environment variables in the `.env` file: ```bash NEXTAUTH_SECRET NEXT_PUBLIC_WEBAPP_URL NEXT_PRIVATE_DATABASE_URL NEXT_PRIVATE_DIRECT_DATABASE_URL NEXT_PRIVATE_SMTP_FROM_NAME NEXT_PRIVATE_SMTP_FROM_ADDRESS ``` Alternatively, you can run `cp .env.example .env` to get started with our handpicked defaults. See the [Environment Variables](/docs/self-hosting/configuration/environment) page for more information. ### Create database schema [#create-database-schema] Create the database schema by running the following command: ```bash npm run prisma:migrate-dev ``` ### Optional: seed the database [#optional-seed-the-database] Seed the database with test data by running the following command: ```bash npm run prisma:seed -w @documenso/prisma ``` ### Optional: configure job provider [#optional-configure-job-provider] The default local job provider does not support scheduled jobs required for document reminders. See the [Background Jobs](/docs/self-hosting/configuration/background-jobs) page for more information. ### Start the application [#start-the-application] Run `npm run dev` in the root directory to start the application. ### Access the application [#access-the-application] Access the Documenso application by visiting `http://localhost:3000` in your web browser. Optional: Create your signing certificate. To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL), see **[Create your signing certificate](/docs/developers/local-development/signing-certificate)**. ## Running Scripts with Environment Variables [#running-scripts-with-environment-variables] If a package script does not automatically load your `.env` and `.env.local` files, wrap it with the `with:env` script: ```bash npm run with:env -- npm run myscript ``` The same works for `npx` when running bin scripts: ```bash npm run with:env -- npx myscript ``` ## See Also [#see-also] * [Developer Quickstart](/docs/developers/local-development/quickstart) - Quick Docker-based setup * [Signing Certificate](/docs/developers/local-development/signing-certificate) - Create a signing certificate # Developer Quickstart ## Quickstart [#quickstart] **Note**: This guide assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine. Want to get up and running quickly? Follow these steps: {/* prettier-ignore */} ### Fork Documenso [#fork-documenso] Fork the [Documenso repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account. ### Clone repository [#clone-repository] After forking the repository, clone it to your local device by using the following command: ```bash git clone https://github.com//documenso ``` ### Set up environment variables [#set-up-environment-variables] Set up your environment variables in the `.env` file using the `.env.example` file as a reference. Alternatively, you can run `cp .env.example .env` to get started with our handpicked defaults. ### Start database and mail server [#start-database-and-mail-server] Run `npm run dx` in the root directory. This will spin up a Postgres database and inbucket mailserver in a docker container. ### Start the application [#start-the-application] Run `npm run dev` in the root directory to start the application. ### (Optional) Fasten the Process [#optional-fasten-the-process] Want it even faster? Just use: ```sh npm run d ``` ### Access Points for the Project [#access-points-for-the-project] You can access the following services: * Main application - [http://localhost:3000](http://localhost:3000) * Incoming Mail Access - [http://localhost:9000](http://localhost:9000) * Database Connection Details: * Port: 54320 * Connection: Use your favorite database client to connect to the database. * S3 Storage Dashboard - [http://localhost:9001](http://localhost:9001) ## See Also [#see-also] * [Manual Setup](/docs/developers/local-development/manual) - Set up without Docker * [Signing Certificate](/docs/developers/local-development/signing-certificate) - Create a certificate for local development # Signing Certificate # Create Your Signing Certificate [#create-your-signing-certificate] Digitally signing documents requires a signing certificate in `.p12` format. You can either purchase one or create a free self-signed certificate. Follow the steps below to create a free, self-signed certificate for local development. These steps should be run on a UNIX based system, otherwise you may run into an error. {/* prettier-ignore */} ### Generate private key [#generate-private-key] Generate a private key using OpenSSL by running the following command: ```bash openssl genrsa -out private.key 2048 ``` This command generates a 2048-bit RSA key. ### Generate self-signed certificate [#generate-self-signed-certificate] Using the private key, generate a self-signed certificate by running the following command: ```bash openssl req -new -x509 -key private.key -out certificate.crt -days 365 ``` You will be prompted to enter some information, such as the certificate's Common Name (CN). Ensure that you provide the correct details. The `—days` parameter specifies the certificate's validity period. ### Create `p12` certificate [#create-p12-certificate] Combine the private key and the self-signed certificate to create a `.p12` certificate. Use the following command: ```bash openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt -legacy ``` When running the application in Docker, you may encounter permission issues when attempting to sign documents using your certificate (.p12) file. This happens because the application runs as a non-root user inside the container and needs read access to the certificate. To resolve this, you'll need to update the certificate file permissions to allow the container user 1001, which runs NextJS, to read it: ```bash sudo chown 1001 certificate.p12 ``` ### `p12` certificate password [#p12-certificate-password] When you create the `.p12` certificate, you will be prompted to enter a password. Enter a strong password and keep it secure. Remember this password, as it will be required when using the certificate. Note that for local development, the password can be left empty. ### Add certificate to the project [#add-certificate-to-the-project] Use the `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` environment variable to point at the certificate you created. Details about environment variables associated with certificates can be found [here](/docs/self-hosting/configuration/signing-certificate). ## See Also [#see-also] * [Signing Certificates (Self-Hosting)](/docs/self-hosting/configuration/signing-certificate) - Production certificate configuration * [Signing Certificates (Concepts)](/docs/concepts/signing-certificates) - How digital signing works # Translations ## Overview [#overview] Documenso uses the following stack to handle translations: * [Lingui](https://lingui.dev/) - React i10n library * [Crowdin](https://crowdin.com/) - Handles syncing translations * [OpenAI](https://openai.com/) - Provides AI translations Additional reading can be found in the [Lingui documentation](https://lingui.dev/introduction). ## Quick Guide [#quick-guide] If you require more in-depth information, please see the [Lingui documentation](https://lingui.dev/introduction). ### HTML [#html] Wrap all text to translate in **``** tags exported from **@lingui/react/macro**. ```html

Title

``` For text that is broken into elements, but represent a whole sentence, you must wrap it in a Trans tag so ensure the full message is extracted correctly. ```html

This is one full sentence

``` ### Constants outside of react components [#constants-outside-of-react-components] ```tsx import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; // Wrap text in msg`text to translate` when it's in a constant here, or another file/package. export const CONSTANT_WITH_MSG = { foo: msg`Hello`, bar: msg`World`, }; export const SomeComponent = () => { const { _ } = useLingui(); return (
{/* This will render the correct translated text. */}

{_(CONSTANT_WITH_MSG.foo)}

); }; ``` ### Plurals [#plurals] Lingui provides a Plural component to make it easy. See full documentation [here.](https://lingui.dev/ref/macro#plural-1) ```tsx // Basic usage. ``` ### Dates [#dates] Lingui provides a [DateTime instance](https://lingui.dev/ref/core#i18n.date) with the configured locale. ```tsx import { Trans } from '@lingui/macro'; import { useLingui } from '@lingui/react'; export const SomeComponent = () => { const { i18n } = useLingui(); return The current date is {i18n.date(new Date(), { dateStyle: 'short' })}; }; ``` ## See Also [#see-also] * [Contributing Translations](/docs/developers/contributing/contributing-translations) - Help translate Documenso # Webhook Events ## Event Payload Structure [#event-payload-structure] All webhook events share a common structure: ```json { "event": "DOCUMENT_COMPLETED", "payload": { // Document or template data with recipients }, "createdAt": "2024-04-22T11:52:18.277Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### Top-Level Fields [#top-level-fields] | Field | Type | Description | | ----------------- | -------- | ------------------------------------------------ | | `event` | string | Event type identifier (e.g., `DOCUMENT_CREATED`) | | `payload` | object | Document object with metadata and recipients | | `createdAt` | datetime | When the webhook event was created | | `webhookEndpoint` | string | The URL receiving this webhook | ### Payload Fields [#payload-fields] | Field | Type | Description | | -------------- | --------- | ------------------------------------------------------------------------ | | `id` | number | Legacy numeric v1 document or template ID | | `envelopeId` | string | Canonical v2 identifier (`envelope_` + 16 characters) | | `externalId` | string? | External identifier for integration | | `userId` | number | Owner's user ID | | `authOptions` | object? | Document-level authentication options | | `formValues` | object? | PDF form values associated with the document | | `title` | string | Document or template title | | `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | | `visibility` | string | Document visibility setting | | `createdAt` | datetime | Document creation timestamp | | `updatedAt` | datetime | Last modification timestamp | | `completedAt` | datetime? | Completion timestamp (when all recipients have signed) | | `deletedAt` | datetime? | Deletion timestamp | | `teamId` | number? | Team ID if document belongs to a team | | `templateId` | number? | Template ID if created from a template | | `source` | string | Source: `DOCUMENT`, `TEMPLATE`, or `TEMPLATE_DIRECT_LINK` | | `documentMeta` | object? | Nullable document metadata (subject, message, signing options) | | `recipients` | array | List of recipient objects | | `Recipient` | array | List of recipient objects (legacy, same as recipients) | ### Document Metadata Fields [#document-metadata-fields] | Field | Type | Description | | ------------------------ | ------- | ------------------------------------------ | | `id` | string | Metadata record identifier | | `subject` | string? | Email subject line | | `message` | string? | Email message body | | `timezone` | string | Timezone for date display | | `dateFormat` | string | Date format string | | `redirectUrl` | string? | URL to redirect after signing | | `signingOrder` | string | `PARALLEL` or `SEQUENTIAL` | | `allowDictateNextSigner` | boolean | Whether signers can choose the next signer | | `typedSignatureEnabled` | boolean | Whether typed signatures are allowed | | `uploadSignatureEnabled` | boolean | Whether uploaded signatures are allowed | | `drawSignatureEnabled` | boolean | Whether drawn signatures are allowed | | `language` | string | Document language code | | `distributionMethod` | string | How document is distributed | | `emailSettings` | object? | Custom email settings for this document | ### Recipient Fields [#recipient-fields] | Field | Type | Description | | ---------------------- | --------- | ------------------------------------------------------- | | `id` | number | Recipient ID | | `envelopeId` | string | Canonical parent envelope ID | | `documentId` | number? | Legacy parent document ID; null for templates | | `templateId` | number? | Legacy parent template ID; null for documents | | `email` | string | Recipient email address | | `name` | string | Recipient name | | `token` | string | Unique signing token | | `documentDeletedAt` | datetime? | When the recipient hid the document | | `expiresAt` | datetime? | When the recipient's signing link expires | | `expirationNotifiedAt` | datetime? | When the expiration notification was sent | | `signedAt` | datetime? | When recipient signed | | `authOptions` | object? | Per-recipient authentication options | | `role` | string | Role: `SIGNER`, `VIEWER`, `APPROVER`, `ASSISTANT`, `CC` | | `signingOrder` | number? | Position in signing sequence | | `readStatus` | string | `NOT_OPENED` or `OPENED` | | `signingStatus` | string | `NOT_SIGNED`, `SIGNED`, or `REJECTED` | | `sendStatus` | string | `NOT_SENT` or `SENT` | | `rejectionReason` | string? | Reason if recipient rejected | Use `recipient.envelopeId` as the reliable parent link. The legacy `documentId` and `templateId` fields depend on the parent envelope type, so one of them is always null. *** ## Document Lifecycle Events [#document-lifecycle-events] These events track the document through its lifecycle. ### `document.created` [#documentcreated] Triggered when a new document is created. **Event name:** `DOCUMENT_CREATED` ```json { "event": "DOCUMENT_CREATED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 1, "authOptions": null, "formValues": null, "visibility": "EVERYONE", "title": "contract.pdf", "status": "DRAFT", "createdAt": "2024-04-22T11:44:43.341Z", "updatedAt": "2024-04-22T11:44:43.341Z", "completedAt": null, "deletedAt": null, "teamId": null, "templateId": null, "source": "DOCUMENT", "documentMeta": { "id": "doc_meta_123", "subject": "Please sign this document", "message": "Hello, please review and sign this document.", "timezone": "Etc/UTC", "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": null, "signingOrder": "PARALLEL", "allowDictateNextSigner": false, "typedSignatureEnabled": true, "uploadSignatureEnabled": true, "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": null, "authOptions": null, "signingOrder": 1, "rejectionReason": null, "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "NOT_SENT" } ], "Recipient": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": null, "authOptions": null, "signingOrder": 1, "rejectionReason": null, "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "NOT_SENT" } ] }, "createdAt": "2024-04-22T11:44:44.779Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.sent` [#documentsent] Triggered when a document is sent to recipients for signing. **Event name:** `DOCUMENT_SENT` The document status changes to `PENDING` and recipients have `sendStatus: "SENT"`. ```json { "event": "DOCUMENT_SENT", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 1, "authOptions": null, "formValues": null, "visibility": "EVERYONE", "title": "contract.pdf", "status": "PENDING", "createdAt": "2024-04-22T11:44:43.341Z", "updatedAt": "2024-04-22T11:48:07.569Z", "completedAt": null, "deletedAt": null, "teamId": null, "templateId": null, "source": "DOCUMENT", "documentMeta": { "id": "doc_meta_123", "subject": "Please sign this document", "message": "Hello, please review and sign this document.", "timezone": "Etc/UTC", "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": null, "signingOrder": "PARALLEL", "allowDictateNextSigner": false, "typedSignatureEnabled": true, "uploadSignatureEnabled": true, "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": null, "authOptions": null, "signingOrder": 1, "rejectionReason": null, "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ], "Recipient": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": null, "authOptions": null, "signingOrder": 1, "rejectionReason": null, "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:48:07.945Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.opened` [#documentopened] Triggered when a recipient opens the document for the first time. **Event name:** `DOCUMENT_OPENED` The recipient's `readStatus` changes to `OPENED`. ```json { "event": "DOCUMENT_OPENED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "readStatus": "OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:50:26.174Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.signed` [#documentsigned] Triggered when a recipient signs the document. This fires for each individual signature, not just when the document is fully completed. **Event name:** `DOCUMENT_SIGNED` The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated. ```json { "event": "DOCUMENT_SIGNED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "status": "COMPLETED", "title": "contract.pdf", "source": "DOCUMENT", "completedAt": "2024-04-22T11:52:05.707Z", "recipients": [ { "id": 51, "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "signedAt": "2024-04-22T11:52:05.688Z", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:52:18.577Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.recipient.completed` [#documentrecipientcompleted] Triggered when an individual recipient completes their required action (signing, approving, or viewing). This is useful for tracking per-recipient progress in documents with multiple recipients. **Event name:** `DOCUMENT_RECIPIENT_COMPLETED` ```json { "event": "DOCUMENT_RECIPIENT_COMPLETED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "signedAt": "2024-04-22T11:52:05.688Z", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:52:06.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.completed` [#documentcompleted] Triggered when all recipients have completed their required actions. **Event name:** `DOCUMENT_COMPLETED` The document status changes to `COMPLETED` and `completedAt` is set. ```json { "event": "DOCUMENT_COMPLETED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 1, "authOptions": null, "formValues": null, "visibility": "EVERYONE", "title": "contract.pdf", "status": "COMPLETED", "createdAt": "2024-04-22T11:44:43.341Z", "updatedAt": "2024-04-22T11:52:05.708Z", "completedAt": "2024-04-22T11:52:05.707Z", "deletedAt": null, "teamId": null, "templateId": null, "source": "DOCUMENT", "documentMeta": { "id": "doc_meta_123", "subject": "Please sign this document", "message": "Hello, please review and sign this document.", "timezone": "Etc/UTC", "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": null, "signingOrder": "PARALLEL", "allowDictateNextSigner": false, "typedSignatureEnabled": true, "uploadSignatureEnabled": true, "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, "recipients": [ { "id": 50, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "reviewer@example.com", "name": "Jane Smith", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": "2024-04-22T11:51:10.055Z", "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 1, "rejectionReason": null, "role": "VIEWER", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" }, { "id": 51, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "HkrptwS42ZBXdRKj1TyUo", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": "2024-04-22T11:52:05.688Z", "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 2, "rejectionReason": null, "role": "SIGNER", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" } ], "Recipient": [ { "id": 50, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "reviewer@example.com", "name": "Jane Smith", "token": "vbT8hi3jKQmrFP_LN1WcS", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": "2024-04-22T11:51:10.055Z", "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 1, "rejectionReason": null, "role": "VIEWER", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" }, { "id": 51, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "HkrptwS42ZBXdRKj1TyUo", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": "2024-04-22T11:52:05.688Z", "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 2, "rejectionReason": null, "role": "SIGNER", "readStatus": "OPENED", "signingStatus": "SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:52:18.277Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.rejected` [#documentrejected] Triggered when a recipient rejects the document. **Event name:** `DOCUMENT_REJECTED` The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` contains their reason. ```json { "event": "DOCUMENT_REJECTED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "signedAt": "2024-04-22T11:48:07.569Z", "rejectionReason": "I do not agree with the terms", "readStatus": "OPENED", "signingStatus": "REJECTED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:48:07.945Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.cancelled` [#documentcancelled] Triggered when a pending document is explicitly cancelled with `POST /envelope/cancel`, or when a document owner or team member deletes a document. Deleting a draft or pending document hard-deletes it, while deleting a completed document soft-deletes it. This event is **not** triggered when a recipient hides a document from their inbox. **Event name:** `DOCUMENT_CANCELLED` ```json { "event": "DOCUMENT_CANCELLED", "payload": { "id": 7, "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 3, "authOptions": null, "formValues": null, "visibility": "EVERYONE", "title": "contract.pdf", "status": "PENDING", "createdAt": "2025-01-27T11:02:14.393Z", "updatedAt": "2025-01-27T11:03:16.387Z", "completedAt": null, "deletedAt": null, "teamId": null, "templateId": null, "source": "DOCUMENT", "documentMeta": { "id": "cm6exvn96006ji02rqvzjvwoy", "subject": "", "message": "", "timezone": "Etc/UTC", "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": "", "signingOrder": "PARALLEL", "allowDictateNextSigner": false, "typedSignatureEnabled": true, "uploadSignatureEnabled": true, "drawSignatureEnabled": true, "language": "en", "distributionMethod": "EMAIL", "emailSettings": null }, "recipients": [ { "id": 7, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 7, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "XkKx1HCs6Znm2UBJA2j6o", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": null, "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 1, "rejectionReason": null, "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ], "Recipient": [ { "id": 7, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 7, "templateId": null, "email": "signer@example.com", "name": "John Doe", "token": "XkKx1HCs6Znm2UBJA2j6o", "documentDeletedAt": null, "expiresAt": null, "expirationNotifiedAt": null, "signedAt": null, "authOptions": { "accessAuth": null, "actionAuth": null }, "signingOrder": 1, "rejectionReason": null, "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2025-01-27T11:03:27.730Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `recipient.expired` [#recipientexpired] Triggered when a recipient's signing deadline passes on a pending document before they sign or reject it. **Event name:** `RECIPIENT_EXPIRED` The recipient's `expiresAt` contains the signing deadline, and `expirationNotifiedAt` is set when the expiration is processed. ```json { "event": "RECIPIENT_EXPIRED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "expiresAt": "2024-04-22T11:51:00.000Z", "expirationNotifiedAt": "2024-04-22T11:52:00.000Z", "readStatus": "OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-22T11:52:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `document.reminder.sent` [#documentremindersent] Triggered when a reminder email is sent to a recipient who has not yet completed their action. **Event name:** `DOCUMENT_REMINDER_SENT` ```json { "event": "DOCUMENT_REMINDER_SENT", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", "readStatus": "NOT_OPENED", "signingStatus": "NOT_SIGNED", "sendStatus": "SENT" } ] }, "createdAt": "2024-04-23T09:00:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` *** ## Template Events [#template-events] Template events track changes to reusable document templates. Template payloads use the same structure as document payloads. For `TEMPLATE_CREATED`, `TEMPLATE_UPDATED`, and `TEMPLATE_DELETED` the template's own legacy numeric ID is in `id` and `templateId` is `null`. Only `TEMPLATE_USED` — whose payload describes the new document envelope created from the template — carries the originating template's legacy ID in `templateId`, with `source` set to `TEMPLATE`. ### `template.created` [#templatecreated] Triggered when a new template is created. **Event name:** `TEMPLATE_CREATED` ```json { "event": "TEMPLATE_CREATED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "title": "My Template", "status": "DRAFT", "templateId": null, "source": "TEMPLATE", "recipients": [] }, "createdAt": "2024-04-22T11:44:44.779Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `template.updated` [#templateupdated] Triggered when a template's settings, recipients, or fields are modified. **Event name:** `TEMPLATE_UPDATED` ```json { "event": "TEMPLATE_UPDATED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "title": "My Updated Template", "status": "DRAFT", "templateId": null, "source": "TEMPLATE", "recipients": [] }, "createdAt": "2024-04-22T12:00:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `template.deleted` [#templatedeleted] Triggered when a template is deleted. **Event name:** `TEMPLATE_DELETED` ```json { "event": "TEMPLATE_DELETED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "title": "Deleted Template", "status": "DRAFT", "templateId": null, "source": "TEMPLATE", "recipients": [] }, "createdAt": "2024-04-22T13:00:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` ### `template.used` [#templateused] Triggered when a document is created from a template. This event fires alongside `document.created`, giving you a way to specifically track template usage. **Event name:** `TEMPLATE_USED` ```json { "event": "TEMPLATE_USED", "payload": { "id": 10, "envelopeId": "envelope_abcdefhiklmnorst", "title": "Document from Template", "status": "DRAFT", "templateId": 10, "source": "TEMPLATE", "recipients": [] }, "createdAt": "2024-04-22T14:00:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` *** ## Event Summary [#event-summary] ### Document Events [#document-events] | Event | Trigger | Key Changes | | ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `DOCUMENT_CREATED` | Document uploaded or created from template | `status: "DRAFT"` | | `DOCUMENT_SENT` | Document sent to recipients | `status: "PENDING"`, recipients `sendStatus: "SENT"` | | `DOCUMENT_OPENED` | Recipient opens document for the first time | Recipient `readStatus: "OPENED"` | | `DOCUMENT_SIGNED` | Recipient signs document | Recipient `signingStatus: "SIGNED"`, `signedAt` set | | `DOCUMENT_RECIPIENT_COMPLETED` | Recipient completes their action | Recipient `signingStatus: "SIGNED"`, `signedAt` set | | `DOCUMENT_COMPLETED` | All recipients complete actions | `status: "COMPLETED"`, `completedAt` set | | `DOCUMENT_REJECTED` | Recipient rejects document | Recipient `signingStatus: "REJECTED"`, `rejectionReason` set | | `DOCUMENT_CANCELLED` | Pending document explicitly cancelled, or document deleted | `status: "CANCELLED"` after explicit cancellation; deletion may remove or soft-delete the document | | `RECIPIENT_EXPIRED` | Recipient signing deadline passes | Recipient `expiresAt` passed, `expirationNotifiedAt` set | | `DOCUMENT_REMINDER_SENT` | Reminder email sent to recipient | No status changes | ### Template Events [#template-events-1] | Event | Trigger | Key Changes | | ------------------ | ------------------------------------ | -------------------- | | `TEMPLATE_CREATED` | New template created | `source: "TEMPLATE"` | | `TEMPLATE_UPDATED` | Template settings or fields modified | `source: "TEMPLATE"` | | `TEMPLATE_DELETED` | Template deleted | `source: "TEMPLATE"` | | `TEMPLATE_USED` | Document created from template | `source: "TEMPLATE"` | *** ## Handling Events [#handling-events] When processing webhook events: {/* prettier-ignore */} **Verify the signature** — Check the `X-Documenso-Secret` header matches your configured secret **Check event type** — Use the `event` field to determine the action **Process idempotently** — Webhooks may be retried, so handle duplicate events **Respond quickly** — Return a `2xx` status code within 10 seconds ```typescript app.post('/webhook', (req, res) => { const secret = req.headers['x-documenso-secret']; if (secret !== process.env.WEBHOOK_SECRET) { return res.status(401).send('Unauthorized'); } const { event, payload } = req.body; switch (event) { case 'DOCUMENT_COMPLETED': console.log(`Document ${payload.id} completed`); break; case 'DOCUMENT_RECIPIENT_COMPLETED': const signer = payload.recipients.find((r) => r.signingStatus === 'SIGNED'); console.log(`${signer?.name} completed their action on document ${payload.id}`); break; case 'DOCUMENT_SIGNED': console.log(`Signature added to document ${payload.id}`); break; case 'DOCUMENT_REJECTED': const rejecter = payload.recipients.find((r) => r.signingStatus === 'REJECTED'); console.log(`${rejecter?.name} rejected: ${rejecter?.rejectionReason}`); break; case 'TEMPLATE_USED': console.log(`Template ${payload.templateId} used to create document ${payload.id}`); break; } res.status(200).send('OK'); }); ``` *** ## See Also [#see-also] * [Webhook Setup](/docs/developers/webhooks/setup) - Configure webhook endpoints * [Webhook Verification](/docs/developers/webhooks/verification) - Verify webhook signatures # Webhooks ## How Webhooks Work [#how-webhooks-work] 1. You configure a webhook URL in Documenso 2. When an event occurs, Documenso sends an HTTP POST to your URL 3. Your application processes the event and responds with 200 OK Documenso supports webhook events for the full document lifecycle (created, sent, opened, signed, completed, rejected, cancelled), recipient-level events (recipient completed, reminder sent, recipient expired), and template events (created, updated, deleted, used). *** ## Getting Started [#getting-started] *** ## Example Payload [#example-payload] ```json { "event": "DOCUMENT_COMPLETED", "payload": { "id": 123, "envelopeId": "envelope_abcdefhiklmnorst", "title": "Contract", "status": "COMPLETED", "completedAt": "2024-01-15T10:30:00.000Z", "recipients": [ { "id": 1, "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "signingStatus": "SIGNED" } ] }, "createdAt": "2024-01-15T10:30:00.000Z", "webhookEndpoint": "https://your-endpoint.com/webhook" } ``` `payload.id` is the legacy numeric v1 ID. Use `payload.envelopeId` as the canonical v2 identifier. Each recipient repeats `envelopeId` as the reliable parent link because the legacy `documentId` and `templateId` fields depend on the parent envelope type, leaving one of them null. *** ## See Also [#see-also] * [Document Lifecycle](/docs/concepts/document-lifecycle) - Understanding document statuses # Webhook Setup ## Overview [#overview] Webhooks are HTTP callbacks triggered by specific events in Documenso. When an event occurs (such as a document being signed), Documenso sends an HTTP POST request to your configured URL with details about the event. Common use cases include: * Syncing document status with your database * Triggering automated workflows when documents are signed * Integrating with CRM systems or other third-party services * Sending custom notifications to stakeholders Webhooks are available for teams only. Personal accounts cannot configure webhooks. ## Creating a Webhook Endpoint [#creating-a-webhook-endpoint] Before configuring a webhook in Documenso, you need an endpoint that can receive HTTP POST requests. Here's a minimal example: ```javascript const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhooks/documenso', (req, res) => { const { event, payload, createdAt } = req.body; console.log(`Received event: ${event}`); console.log(`Document ID: ${payload.id}`); console.log(`Document title: ${payload.title}`); // Process the webhook event switch (event) { case 'DOCUMENT_COMPLETED': // Handle completed document break; case 'DOCUMENT_SIGNED': // Handle signed document break; // Handle other events... } // Respond with 200 OK to acknowledge receipt res.status(200).json({ received: true }); }); app.listen(3000, () => { console.log('Webhook server running on port 3000'); }); ``` ```python from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/webhooks/documenso', methods=['POST']) def handle_webhook(): data = request.get_json() event = data.get('event') payload = data.get('payload') print(f"Received event: {event}") print(f"Document ID: {payload.get('id')}") print(f"Document title: {payload.get('title')}") # Process the webhook event if event == 'DOCUMENT_COMPLETED': # Handle completed document pass elif event == 'DOCUMENT_SIGNED': # Handle signed document pass # Respond with 200 OK to acknowledge receipt return jsonify({'received': True}), 200 if __name__ == '__main__': app.run(port=3000) ``` ```go package main import ( "encoding/json" "fmt" "net/http" ) type WebhookPayload struct { Event string `json:"event"` Payload map[string]interface{} `json:"payload"` CreatedAt string `json:"createdAt"` } func webhookHandler(w http.ResponseWriter, r *http.Request) { var data WebhookPayload if err := json.NewDecoder(r.Body).Decode(&data); err != nil { http.Error(w, "Invalid payload", http.StatusBadRequest) return } fmt.Printf("Received event: %s\n", data.Event) fmt.Printf("Document ID: %v\n", data.Payload["id"]) fmt.Printf("Document title: %v\n", data.Payload["title"]) // Process the webhook event switch data.Event { case "DOCUMENT_COMPLETED": // Handle completed document case "DOCUMENT_SIGNED": // Handle signed document } // Respond with 200 OK to acknowledge receipt w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]bool{"received": true}) } func main() { http.HandleFunc("/webhooks/documenso", webhookHandler) fmt.Println("Webhook server running on port 3000") http.ListenAndServe(":3000", nil) } ``` Always respond with a `2xx` status within 10 seconds. Documenso will retry failed deliveries according to the configured background-job provider. ## Configuring Webhooks in Documenso via the Dashboard [#configuring-webhooks-in-documenso-via-the-dashboard] {/* prettier-ignore */} ### Navigate to team settings [#navigate-to-team-settings] Click your avatar in the top right corner and select **Team settings** from the dropdown menu. ### Open the webhooks tab [#open-the-webhooks-tab] Navigate to the **Webhooks** tab in the team settings sidebar. Webhooks settings page ### Create a new webhook [#create-a-new-webhook] Click the **Create Webhook** button to open the configuration dialog. Create webhook dialog ### Configure the webhook [#configure-the-webhook] Fill in the following fields: | Field | Description | | --------------------- | ----------------------------------------------------------- | | **Webhook URL** | The HTTP or HTTPS endpoint that will receive webhook events | | **Events** | Select which events should trigger this webhook | | **Secret** (optional) | A secret key used to sign the payload for verification | ### Save the webhook [#save-the-webhook] Click **Create Webhook** to save your configuration. The webhook is now active and will receive events. ## Webhook URL Requirements [#webhook-url-requirements] Your webhook endpoint must meet these requirements: | Requirement | Details | | ---------------- | ---------------------------------------------------- | | **Protocol** | HTTP and HTTPS are accepted; use HTTPS in production | | **Response** | Must return a `2xx` status code within 10 seconds | | **Method** | Must accept HTTP POST requests | | **Content-Type** | Must accept `application/json` payloads | | **Availability** | Must be publicly accessible from the internet | Documenso performs a best-effort check that rejects webhook URLs which use or resolve to private or loopback addresses. This is not a complete SSRF mitigation — it does not cover DNS rebinding and fails open on DNS lookup errors or timeouts — so self-hosted deployments should still enforce network-level egress rules. Self-hosters that need to deliver to a hostname resolving to a private address can add that hostname to the comma-separated `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` environment variable. For local development, use a tunneling service like [ngrok](https://ngrok.com) or [localtunnel](https://localtunnel.me) to expose your local server. ## Selecting Events [#selecting-events] When creating a webhook, you can subscribe to one or more events: | Event | Trigger | | ------------------------------ | ------------------------------------------------------------------------- | | `DOCUMENT_CREATED` | A new document is created | | `DOCUMENT_SENT` | A document is sent to recipients | | `DOCUMENT_OPENED` | A recipient opens the document for the first time | | `DOCUMENT_SIGNED` | A recipient signs the document | | `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action | | `DOCUMENT_COMPLETED` | All recipients have completed their actions | | `DOCUMENT_REJECTED` | A recipient rejects the document | | `DOCUMENT_CANCELLED` | A pending document is explicitly cancelled or a document owner deletes it | | `RECIPIENT_EXPIRED` | A recipient's signing deadline passes before they sign or reject | | `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient | | `TEMPLATE_CREATED` | A new template is created | | `TEMPLATE_UPDATED` | A template is modified | | `TEMPLATE_DELETED` | A template is deleted | | `TEMPLATE_USED` | A document is created from a template | You can subscribe to all events or select specific ones based on your needs. For example, if you only need to know when documents are fully signed, subscribe only to `DOCUMENT_COMPLETED`. See [Webhook Events](/docs/developers/webhooks/events) for detailed payload information for each event type. ## Testing Webhooks [#testing-webhooks] Documenso provides a built-in testing feature to verify your webhook endpoint works correctly. {/* prettier-ignore */} ### Navigate to webhook details [#navigate-to-webhook-details] Go to **Team Settings > Webhooks** and click on the webhook you want to test. Webhook detail page ### Click test [#click-test] Click the **Test** button in the webhook details page. ### Select an event type [#select-an-event-type] Choose which event type you want to simulate from the dropdown. ### Send test payload [#send-test-payload] Click **Send** to dispatch a test webhook with sample data to your endpoint. Webhook test trigger The test payload contains realistic sample data so you can verify your endpoint processes events correctly. After sending, you can view the response in the webhook call logs. ### Viewing Webhook Logs [#viewing-webhook-logs] Each webhook subscription maintains a log of all delivery attempts. To view logs: {/* prettier-ignore */} Go to **Team Settings > Webhooks** Click on a webhook to view its details Review the logs Each webhook call shows the following details: * Status (success/failure) * Event type * Timestamp * Response code * Request and response bodies * Response headers Click any call to see full details including headers and response data. ### Resending Failed Webhooks [#resending-failed-webhooks] If a webhook delivery fails, you can manually resend it: {/* prettier-ignore */} Navigate to the webhook call details page Click the **Resend** button Documenso will attempt to deliver the same payload again ## Retry Policy [#retry-policy] A delivery fails when the endpoint returns a non-`2xx` response, the 10-second timeout expires, or the request fails. Redirects are not followed, so `3xx` responses also fail. Network and SSRF-blocked requests are recorded with response code `0`. For self-hosted deployments, retries are handled by the background-job provider selected with `NEXT_PRIVATE_JOBS_PROVIDER`: | Provider | Total attempts | Retry timing | | --------------- | -------------- | ---------------------------------------- | | Local (default) | 4 | Back-to-back, with no backoff | | BullMQ | 3 | Exponential backoff starting at 1 second | | Inngest | 5 | Inngest platform backoff | Only the individual delivery (`WebhookCall`) record is marked as failed. Documenso does not automatically disable the webhook or apply a circuit breaker, so future matching events continue to be delivered. After automatic attempts are exhausted, you can manually resend a failed delivery from the dashboard. If your endpoint consistently fails, consider reviewing your server logs and ensuring your endpoint meets all [URL requirements](#webhook-url-requirements). ## Security Best Practices [#security-best-practices] Always use HTTPS endpoints in production. Use a webhook secret and verify the `X-Documenso-Secret` header (see [Verification](/docs/developers/webhooks/verification)). Validate incoming data before processing. Return 200 OK immediately, then process asynchronously. Handle duplicate deliveries gracefully. ## Next Steps [#next-steps] * [Webhook Events](/docs/developers/webhooks/events) - Detailed payload structure for each event type * [Webhook Verification](/docs/developers/webhooks/verification) - Secure your webhooks with signature verification ``` ``` # Webhook Verification ## Overview [#overview] Verifying webhook requests ensures that incoming payloads originate from Documenso and have not been tampered with. Without verification, attackers could forge requests to your endpoint and trigger unintended actions. ## How Documenso Signs Webhooks [#how-documenso-signs-webhooks] When you configure a webhook with a secret, Documenso includes that secret in every webhook request via the `X-Documenso-Secret` header. Your server should compare this header value against your stored secret to authenticate the request. ```bash POST /your-webhook-endpoint HTTP/1.1 Host: your-server.com Content-Type: application/json X-Documenso-Secret: your_webhook_secret_here {"event": "DOCUMENT_COMPLETED", "payload": {...}} ``` ## Signature Header Format [#signature-header-format] | Header | Description | | -------------------- | ------------------------------------------------------- | | `X-Documenso-Secret` | The secret key you configured when creating the webhook | The header contains your webhook secret as a plain string. If you did not configure a secret, the header will be an empty string. ## Verification Steps [#verification-steps] {/* prettier-ignore */} Extract the `X-Documenso-Secret` header from the incoming request Compare it against your stored webhook secret using a constant-time comparison Reject the request if the values do not match Process the webhook payload if verification succeeds Always use constant-time string comparison to prevent timing attacks. Standard equality operators (`===` or `==`) can leak information about the secret through response time variations. ## Code Examples [#code-examples] ```javascript const express = require('express'); const crypto = require('crypto'); const app = express(); app.use(express.json()); const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET; function verifyWebhookSignature(receivedSecret, expectedSecret) { if (!expectedSecret) { // No secret configured, skip verification // Not recommended for production return true; } if (!receivedSecret) { return false; } // Use constant-time comparison to prevent timing attacks try { return crypto.timingSafeEqual( Buffer.from(receivedSecret), Buffer.from(expectedSecret), ); } catch { return false; } } app.post('/webhooks/documenso', (req, res) => { const receivedSecret = req.headers['x-documenso-secret']; if (!verifyWebhookSignature(receivedSecret, WEBHOOK_SECRET)) { console.error('Webhook verification failed'); return res.status(401).json({ error: 'Invalid signature' }); } // Signature verified, process the webhook const { event, payload } = req.body; console.log(`Verified webhook: ${event}`); // Process the event... res.status(200).json({ received: true }); }); app.listen(3000, () => { console.log('Webhook server running on port 3000'); }); ``` ```python import hmac import os from flask import Flask, request, jsonify app = Flask(__name__) WEBHOOK_SECRET = os.environ.get('DOCUMENSO_WEBHOOK_SECRET') def verify_webhook_signature(received_secret, expected_secret): """Verify the webhook signature using constant-time comparison.""" if not expected_secret: # No secret configured, skip verification # Not recommended for production return True if not received_secret: return False # Use constant-time comparison to prevent timing attacks return hmac.compare_digest(received_secret, expected_secret) @app.route('/webhooks/documenso', methods=['POST']) def handle_webhook(): received_secret = request.headers.get('X-Documenso-Secret', '') if not verify_webhook_signature(received_secret, WEBHOOK_SECRET): print('Webhook verification failed') return jsonify({'error': 'Invalid signature'}), 401 # Signature verified, process the webhook data = request.get_json() event = data.get('event') payload = data.get('payload') print(f'Verified webhook: {event}') # Process the event... return jsonify({'received': True}), 200 if __name__ == '__main__': app.run(port=3000) ``` ## Handling Verification Failures [#handling-verification-failures] When verification fails, follow these practices: | Action | Description | | ------------------- | ------------------------------------------------------------------ | | Return 401 status | Respond with `401 Unauthorized` to indicate authentication failure | | Log the attempt | Record failed attempts for security monitoring | | Do not process | Never process the payload if verification fails | | Do not leak details | Avoid exposing information about why verification failed | ```javascript app.post('/webhooks/documenso', (req, res) => { const receivedSecret = req.headers['x-documenso-secret']; if (!verifyWebhookSignature(receivedSecret, WEBHOOK_SECRET)) { // Log for monitoring but don't expose details console.error('Webhook verification failed', { timestamp: new Date().toISOString(), ip: req.ip, }); // Generic error response return res.status(401).json({ error: 'Unauthorized' }); } // Continue processing... }); ``` ### Common Verification Issues [#common-verification-issues] | Issue | Cause | Solution | | --------------- | --------------------------------------- | ------------------------------------------------------------------ | | Secret mismatch | Webhook secret changed or misconfigured | Verify the secret in your environment matches the one in Documenso | | Empty header | Webhook created without a secret | Add a secret to the webhook configuration in Documenso | | Encoding issues | String encoding mismatch | Ensure both secrets use the same encoding (UTF-8) | ## Security Best Practices [#security-best-practices] Generate a cryptographically secure random string for your webhook secret: ```bash # Generate a 32-byte random secret openssl rand -hex 32 ``` Never hardcode secrets in your source code. Use environment variables or a secrets manager: ```javascript // Good: Environment variable const WEBHOOK_SECRET = process.env.DOCUMENSO_WEBHOOK_SECRET; // Bad: Hardcoded const WEBHOOK_SECRET = 'my-secret-key'; // Never do this ``` Update your webhook secret periodically: 1. Generate a new secret 2. Update your server to accept both old and new secrets temporarily 3. Update the webhook configuration in Documenso 4. Remove the old secret from your server After verifying the signature, validate the payload structure before processing: ```javascript const { event, payload, createdAt } = req.body; if (!event || !payload) { return res.status(400).json({ error: 'Invalid payload structure' }); } // Validate event is a known type const validEvents = [ 'DOCUMENT_CREATED', 'DOCUMENT_SENT', 'DOCUMENT_OPENED', 'DOCUMENT_SIGNED', 'DOCUMENT_RECIPIENT_COMPLETED', 'DOCUMENT_COMPLETED', 'DOCUMENT_REJECTED', 'DOCUMENT_CANCELLED', 'DOCUMENT_REMINDER_SENT', 'RECIPIENT_EXPIRED', 'TEMPLATE_CREATED', 'TEMPLATE_UPDATED', 'TEMPLATE_DELETED', 'TEMPLATE_USED', ]; if (!validEvents.includes(event)) { console.warn(`Unknown event type: ${event}`); } ``` Always use HTTPS endpoints in production to encrypt data in transit, including the secret header. Protect your endpoint from abuse with rate limiting: ```javascript const rateLimit = require('express-rate-limit'); const webhookLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute message: { error: 'Too many requests' }, }); app.post('/webhooks/documenso', webhookLimiter, (req, res) => { // Handle webhook... }); ``` ## See Also [#see-also] * [Webhook Setup](/docs/developers/webhooks/setup) - Configure webhook endpoints and secrets * [Webhook Events](/docs/developers/webhooks/events) - Event types and payload structure # Background Jobs ## Overview [#overview] Documenso processes background jobs for email delivery, document sealing, webhook dispatch, and scheduled maintenance tasks. Three providers are available: | Provider | Backend | Best For | Infrastructure | | -------- | ---------- | --------------------------------------------- | -------------- | | Inngest | Managed | Production with zero ops overhead | None | | BullMQ | Redis | Self-hosted production with full control | Redis | | Local | PostgreSQL | Development and small self-hosted deployments | None | Select a provider with the `NEXT_PRIVATE_JOBS_PROVIDER` environment variable: ```bash NEXT_PRIVATE_JOBS_PROVIDER=inngest # or bullmq, local ``` The default provider is `local`. It requires no additional infrastructure and works well for development and small deployments, but is not recommended for production workloads. *** ## Inngest (Recommended) [#inngest-recommended] [Inngest](https://www.inngest.com/) is a managed background job service. It handles scheduling, retries, concurrency, and observability without any infrastructure to manage. This is the recommended provider for production deployments. ### Setup [#setup] {/* prettier-ignore */} 1. Create an account at [inngest.com](https://www.inngest.com/) 2. Create an app and obtain your event key and signing key 3. Configure the environment variables: ```bash NEXT_PRIVATE_JOBS_PROVIDER=inngest NEXT_PRIVATE_INNGEST_EVENT_KEY=your-event-key INNGEST_SIGNING_KEY=your-signing-key ``` ### Environment Variables [#environment-variables] | Variable | Description | Required | | -------------------------------- | -------------------------------------------- | -------- | | `NEXT_PRIVATE_INNGEST_EVENT_KEY` | Inngest event key | Yes | | `INNGEST_EVENT_KEY` | Alternative Inngest event key | No | | `INNGEST_SIGNING_KEY` | Inngest signing key for webhook verification | Yes | | `NEXT_PRIVATE_INNGEST_APP_ID` | Custom Inngest app ID | No | ### Advantages [#advantages] * No infrastructure to manage * Built-in monitoring dashboard * Automatic retries with backoff * Cron scheduling handled externally * Scales automatically *** ## BullMQ [#bullmq] [BullMQ](https://docs.bullmq.io/) is a Redis-backed job queue that runs inside the Documenso process. It provides higher throughput than the local provider, configurable concurrency, and a built-in dashboard for monitoring jobs. ### Requirements [#requirements] * **Redis 6.2+** - any Redis-compatible service works (Redis, KeyDB, Dragonfly, AWS ElastiCache, Upstash, etc.) ### Setup [#setup-1] ```bash NEXT_PRIVATE_JOBS_PROVIDER=bullmq NEXT_PRIVATE_REDIS_URL=redis://localhost:6379 ``` ### Environment Variables [#environment-variables-1] | Variable | Description | Default | | --------------------------------- | ------------------------------------------------------------- | ------------ | | `NEXT_PRIVATE_REDIS_URL` | Redis connection URL | *(required)* | | `NEXT_PRIVATE_REDIS_PREFIX` | Key prefix for Redis queues (useful when sharing an instance) | `documenso` | | `NEXT_PRIVATE_BULLMQ_CONCURRENCY` | Number of concurrent jobs to process | `10` | ### Dashboard [#dashboard] BullMQ includes a job monitoring dashboard at `/api/jobs/board`. In production, only admin users can access the dashboard. In development, it is open to all users. The dashboard provides visibility into queued, active, completed, and failed jobs. ### Docker Compose with Redis [#docker-compose-with-redis] If you're using Docker Compose, add a Redis service: ```yaml services: redis: image: redis:8-alpine ports: - '6379:6379' volumes: - redis_data:/data volumes: redis_data: ``` Then set `NEXT_PRIVATE_REDIS_URL=redis://redis:6379` in your Documenso environment. ### Advantages [#advantages-1] * Self-hosted with no external service dependencies beyond Redis * Configurable concurrency * Built-in job monitoring dashboard * Reliable retries with exponential backoff * Queue namespacing for shared Redis instances *** ## Local [#local] The local provider uses your PostgreSQL database as a job queue. Jobs are stored in the `BackgroundJob` table and processed via internal HTTP requests that Documenso sends to itself. ### Setup [#setup-2] No configuration required. The local provider is the default when `NEXT_PRIVATE_JOBS_PROVIDER` is unset or set to `local`. ```bash # Optional - this is the default NEXT_PRIVATE_JOBS_PROVIDER=local ``` ### Internal URL [#internal-url] Background jobs in the local provider work by Documenso sending HTTP requests to itself. If your reverse proxy or network setup causes issues with the app reaching its own public URL, set the internal URL: ```bash NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000 ``` This tells the job system to use the internal address instead of `NEXT_PUBLIC_WEBAPP_URL` for self-requests. The local provider is suitable for development and small deployments. For production workloads, use Inngest or BullMQ. ### Limitations [#limitations] * No concurrency control - jobs are processed one at a time per request cycle * No built-in monitoring * Depends on the application being able to reach itself over HTTP * Not suitable for high-throughput workloads *** ## Choosing a Provider [#choosing-a-provider] Use **Inngest**. Zero infrastructure, automatic scaling, and built-in observability. The simplest path to reliable background jobs in production. Use **BullMQ**. Add a Redis instance to your infrastructure and get reliable job processing with a monitoring dashboard. Good fit if you already run Redis or want to keep everything self-hosted. Use **Local** (the default). No additional setup required. Works out of the box with just PostgreSQL. *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference * [Requirements](/docs/self-hosting/getting-started/requirements) - Infrastructure requirements * [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Deploy with Docker Compose # Database Configuration ## Supported Databases [#supported-databases] Documenso requires **PostgreSQL 14 or later**. No other databases are supported. PostgreSQL provides the reliability, performance, and feature set required for document signing workflows, including: * ACID compliance for transaction integrity * JSON support for flexible metadata storage * Full-text search capabilities * Robust backup and replication options ## Connection String Format [#connection-string-format] PostgreSQL connection strings follow this format: ``` postgresql://[user]:[password]@[host]:[port]/[database]?[parameters] ``` ### Components [#components] | Component | Description | Example | | ------------ | ------------------------------- | ----------------- | | `user` | Database username | `documenso` | | `password` | Database password (URL-encoded) | `secretpass` | | `host` | Database server hostname or IP | `localhost` | | `port` | Database port | `5432` | | `database` | Database name | `documenso` | | `parameters` | Additional connection options | `sslmode=require` | ### Examples [#examples] **Local development:** ``` postgresql://documenso:password@localhost:5432/documenso ``` **Remote server with SSL:** ``` postgresql://documenso:password@db.example.com:5432/documenso?sslmode=require ``` **With special characters in password:** URL-encode special characters in passwords. For example, `p@ss#word` becomes `p%40ss%23word`: ``` postgresql://documenso:p%40ss%23word@localhost:5432/documenso ``` ## Environment Variables [#environment-variables] Documenso uses two database connection variables: | Variable | Purpose | | ---------------------------------- | --------------------------------------------------- | | `NEXT_PRIVATE_DATABASE_URL` | Primary connection for application queries | | `NEXT_PRIVATE_DIRECT_DATABASE_URL` | Direct connection for migrations and schema changes | ### Basic Configuration [#basic-configuration] When not using a connection pooler, set both variables to the same value: ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@host:5432/documenso NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@host:5432/documenso ``` ### Automatic Detection [#automatic-detection] Documenso automatically detects common database environment variable formats used by hosting providers: | Provider Variable | Maps To | | -------------------------- | ---------------------------------- | | `DATABASE_URL` | `NEXT_PRIVATE_DATABASE_URL` | | `POSTGRES_URL` | `NEXT_PRIVATE_DATABASE_URL` | | `POSTGRES_PRISMA_URL` | `NEXT_PRIVATE_DATABASE_URL` | | `DATABASE_URL_UNPOOLED` | `NEXT_PRIVATE_DIRECT_DATABASE_URL` | | `POSTGRES_URL_NON_POOLING` | `NEXT_PRIVATE_DIRECT_DATABASE_URL` | If your hosting provider sets these variables, Documenso will use them automatically. ## Connection Pooling [#connection-pooling] Connection pooling improves performance by reusing database connections instead of creating new ones for each request. ### When to Use Pooling [#when-to-use-pooling] Use connection pooling when: * Running multiple application instances * Deploying to serverless environments * Handling high concurrent request volumes * Your database has connection limits ### PgBouncer Configuration [#pgbouncer-configuration] When using PgBouncer or similar poolers, configure two connection strings: 1. **Pooled connection** for application queries 2. **Direct connection** for migrations (bypasses the pooler) ```bash # Pooled connection (through PgBouncer) NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@pooler-host:6432/documenso?pgbouncer=true # Direct connection (bypasses PgBouncer) NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@db-host:5432/documenso ``` Migrations must use a direct connection. Running migrations through a connection pooler will fail. ### Prisma Connection Pool [#prisma-connection-pool] Documenso uses Prisma, which maintains its own connection pool. Configure pool size with connection string parameters: ``` postgresql://user:password@host:5432/documenso?connection_limit=10&pool_timeout=30 ``` | Parameter | Description | Default | | ------------------ | ---------------------------------------- | ------- | | `connection_limit` | Maximum connections in the pool | 10 | | `pool_timeout` | Seconds to wait for available connection | 10 | ## SSL/TLS Connections [#ssltls-connections] ### Enabling SSL [#enabling-ssl] Add SSL parameters to your connection string: ``` postgresql://user:password@host:5432/documenso?sslmode=require ``` ### SSL Modes [#ssl-modes] | Mode | Description | | ------------- | --------------------------------------------------- | | `disable` | No SSL (not recommended for production) | | `allow` | Try non-SSL first, fall back to SSL | | `prefer` | Try SSL first, fall back to non-SSL | | `require` | Require SSL, but don't verify certificate | | `verify-ca` | Require SSL and verify server certificate | | `verify-full` | Require SSL, verify certificate, and check hostname | For production, use `require` at minimum. Use `verify-full` when your CA certificate is available. ### Custom Certificates [#custom-certificates] When connecting to databases with self-signed or private CA certificates: ``` postgresql://user:password@host:5432/documenso?sslmode=verify-full&sslrootcert=/path/to/ca.crt ``` For Docker deployments, mount the certificate file: ```bash docker run -d \ -v /path/to/ca.crt:/etc/ssl/certs/db-ca.crt:ro \ -e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso?sslmode=verify-full&sslrootcert=/etc/ssl/certs/db-ca.crt" \ documenso/documenso:latest ``` ## Running Migrations [#running-migrations] Database migrations update your schema when upgrading Documenso. ### Automatic Migrations [#automatic-migrations] When running Documenso via Docker, migrations run automatically on container startup. No manual intervention is required. ### Manual Migrations [#manual-migrations] For manual deployments or troubleshooting: ```bash # Apply pending migrations npm run prisma:migrate-deploy # Or using npx directly npx prisma migrate deploy ``` Always back up your database before running migrations, especially for major version upgrades. ### Migration Commands [#migration-commands] | Command | Purpose | | ----------------------- | ----------------------------------------- | | `prisma:migrate-deploy` | Apply pending migrations (production) | | `prisma:migrate-dev` | Create and apply migrations (development) | | `prisma:migrate-reset` | Reset database and apply all migrations | ### Troubleshooting Migrations [#troubleshooting-migrations] Check the `_prisma_migrations` table: ```sql SELECT * FROM _prisma_migrations WHERE finished_at IS NULL; ``` To retry, fix the underlying issue (disk space, permissions, etc.), mark as rolled back: ```sql UPDATE _prisma_migrations SET rolled_back_at = NOW() WHERE migration_name = 'failed_migration_name'; ``` Run migrations again. Use the direct database URL and increase timeout: ```bash NEXT_PRIVATE_DIRECT_DATABASE_URL="postgresql://user:password@host:5432/documenso?connect_timeout=60" ``` ## Managed Database Services [#managed-database-services] Supabase provides PostgreSQL with built-in connection pooling via Supavisor. **Configuration:** ```bash # Pooled connection (Session mode - port 5432) NEXT_PRIVATE_DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres # Direct connection for migrations (port 5432, direct host) NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres ``` Find your connection strings in the Supabase dashboard under **Settings > Database > Connection string**. Neon provides serverless PostgreSQL with automatic scaling. **Configuration:** ```bash # Pooled connection NEXT_PRIVATE_DATABASE_URL=postgresql://[user]:[password]@[endpoint]-pooler.region.aws.neon.tech/documenso?sslmode=require # Direct connection for migrations NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://[user]:[password]@[endpoint].region.aws.neon.tech/documenso?sslmode=require ``` The pooler endpoint includes `-pooler` in the hostname. **Configuration:** ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://documenso:[password]@your-instance.region.rds.amazonaws.com:5432/documenso?sslmode=require NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://documenso:[password]@your-instance.region.rds.amazonaws.com:5432/documenso?sslmode=require ``` **Recommended settings:** * Instance class: `db.t3.medium` or larger for production * Storage: General Purpose SSD (gp3), minimum 20GB * Enable automated backups with 7+ day retention * Enable Multi-AZ for high availability **Configuration:** ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://documenso:[password]@/documenso?host=/cloudsql/[project]:[region]:[instance] ``` When connecting via Cloud SQL Proxy, use Unix socket connections. For public IP connections: ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://documenso:[password]@[public-ip]:5432/documenso?sslmode=require ``` **Configuration:** ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://documenso@[server-name]:[password]@[server-name].postgres.database.azure.com:5432/documenso?sslmode=require ``` Note: Azure requires the username format `user@servername`. **Configuration:** ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://doadmin:[password]@[cluster-host]:25060/documenso?sslmode=require ``` DigitalOcean uses port 25060 by default and requires SSL. ## Backup Recommendations [#backup-recommendations] ### Backup Strategies [#backup-strategies] | Strategy | Frequency | Retention | Use Case | | ---------------------- | ------------ | ----------- | ---------------------- | | Automated snapshots | Daily | 7-30 days | Point-in-time recovery | | Logical backups | Daily/Weekly | 30-90 days | Long-term retention | | Continuous replication | Real-time | 24-72 hours | Disaster recovery | ### PostgreSQL Backup Commands [#postgresql-backup-commands] **Create a logical backup:** ```bash pg_dump -h host -U user -d documenso -F c -f documenso_backup.dump ``` **Restore from backup:** ```bash pg_restore -h host -U user -d documenso -c documenso_backup.dump ``` ### Managed Service Backups [#managed-service-backups] Most managed database services provide automated backups: Daily backups with point-in-time recovery (Pro plan) Automatic branching for instant recovery Automated backups with configurable retention Automated and on-demand backups Automatic backups with geo-redundancy options Always test your backup restoration process. Untested backups may not work when needed. ## Performance Tuning [#performance-tuning] ### PostgreSQL Configuration [#postgresql-configuration] Key parameters for Documenso workloads: | Parameter | Recommended Value | Description | | ---------------------- | ----------------- | ------------------------------------- | | `shared_buffers` | 25% of RAM | Memory for caching data | | `effective_cache_size` | 75% of RAM | Planner's estimate of available cache | | `work_mem` | 64MB-256MB | Memory per sort/hash operation | | `maintenance_work_mem` | 512MB-1GB | Memory for maintenance operations | | `max_connections` | 100-200 | Maximum concurrent connections | ### Connection Limits [#connection-limits] Calculate your connection limit: ``` max_connections = (application_instances × connection_pool_size) + admin_overhead ``` Example: 3 app instances with pool size 10 = `(3 × 10) + 10 = 40` connections minimum. ### Indexing [#indexing] Documenso includes necessary indexes by default. Additional indexes may help for: * Custom reporting queries * High-volume document searches * Audit log analysis Check for slow queries: ```sql SELECT query, calls, mean_time, total_time FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10; ``` ### Monitoring [#monitoring] Monitor these metrics: | Metric | Warning Threshold | Action | | -------------------- | ----------------- | ------------------------------- | | Connection usage | > 80% | Increase limits or add pooling | | Disk usage | > 80% | Add storage or archive old data | | Cache hit ratio | \< 95% | Increase `shared_buffers` | | Long-running queries | > 30 seconds | Optimize query or add indexes | ## Troubleshooting [#troubleshooting] Causes: * PostgreSQL not running * Incorrect host or port * Firewall blocking Solutions: * Verify PostgreSQL is running with `pg_isready -h host -p 5432` * Check the connection string * Verify firewall rules with `pg_isready -h host -p 5432`, check the connection string, verify firewall rules. Causes: * Incorrect password * User doesn't exist * Password contains special characters Solutions: * Reset password with `ALTER USER documenso WITH PASSWORD 'newpassword';` * Verify credentials in `pg_hba.conf` * Verify URL-encoded special characters Add SSL mode to your connection string: ```bash postgresql://user:password@host:5432/documenso?sslmode=require ``` Causes: * Connection pool exhausted * Connections not released * Multiple instances exceeding limits Solutions: * Reduce `connection_limit` * Increase `max_connections` * Implement PgBouncer Create the database: ```sql CREATE DATABASE documenso; GRANT ALL PRIVILEGES ON DATABASE documenso TO documenso; ``` Grant permissions: ```sql GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO documenso; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO documenso; ``` *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference * [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up document storage * [Backups](/docs/self-hosting/maintenance/backups) - Backup strategies and procedures * [Upgrades](/docs/self-hosting/maintenance/upgrades) - Upgrade procedures and migration handling # Email Configuration ## Supported Email Transports [#supported-email-transports] Documenso supports four email transport methods: | Transport | Use Case | Configuration Complexity | | -------------- | ------------------------------------- | ------------------------ | | `smtp-auth` | Standard SMTP with username/password | Low | | `smtp-api` | SMTP with API key authentication | Low | | `resend` | Resend.com email API | Low | | `mailchannels` | MailChannels API (Cloudflare Workers) | Medium | Select a transport by setting the `NEXT_PRIVATE_SMTP_TRANSPORT` environment variable. The default is `smtp-auth`. ## Sender Configuration [#sender-configuration] All transports require sender configuration: | Variable | Description | Required | | -------------------------------- | --------------------------------- | -------- | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Email address shown as the sender | Yes | | `NEXT_PRIVATE_SMTP_FROM_NAME` | Display name shown as the sender | Yes | ```bash NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@example.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` The sender address must be authorized to send from your email provider. Using an unauthorized address will cause delivery failures. ## SMTP Configuration [#smtp-configuration] SMTP is the most common email transport. Documenso supports two SMTP authentication methods. ### SMTP with Username/Password (smtp-auth) [#smtp-with-usernamepassword-smtp-auth] Use this for standard SMTP servers that authenticate with username and password. **Environment Variables:** | Variable | Description | Default | | ------------------------------------- | ----------------------------------------- | ----------- | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Set to `smtp-auth` | `smtp-auth` | | `NEXT_PRIVATE_SMTP_HOST` | SMTP server hostname | `127.0.0.1` | | `NEXT_PRIVATE_SMTP_PORT` | SMTP server port | `587` | | `NEXT_PRIVATE_SMTP_USERNAME` | Authentication username | | | `NEXT_PRIVATE_SMTP_PASSWORD` | Authentication password | | | `NEXT_PRIVATE_SMTP_SECURE` | Use TLS on connection (`true` or `false`) | `false` | | `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS` | Disable TLS entirely (not recommended) | `false` | | `NEXT_PRIVATE_SMTP_SERVICE` | Nodemailer service preset (e.g., `gmail`) | | **Example Configuration:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="smtp.example.com" NEXT_PRIVATE_SMTP_PORT="587" NEXT_PRIVATE_SMTP_USERNAME="your-username" NEXT_PRIVATE_SMTP_PASSWORD="your-password" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@example.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` ### Common SMTP Provider Examples [#common-smtp-provider-examples] **Gmail / Google Workspace:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="smtp.gmail.com" NEXT_PRIVATE_SMTP_PORT="587" NEXT_PRIVATE_SMTP_USERNAME="your-email@gmail.com" NEXT_PRIVATE_SMTP_PASSWORD="your-app-password" NEXT_PRIVATE_SMTP_FROM_ADDRESS="your-email@gmail.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` Gmail requires an App Password when 2FA is enabled. Generate one at [Google Account Security](https://myaccount.google.com/apppasswords). **Microsoft 365 / Outlook:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="smtp.office365.com" NEXT_PRIVATE_SMTP_PORT="587" NEXT_PRIVATE_SMTP_USERNAME="your-email@yourdomain.com" NEXT_PRIVATE_SMTP_PASSWORD="your-password" NEXT_PRIVATE_SMTP_FROM_ADDRESS="your-email@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` **Amazon SES:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="email-smtp.us-east-1.amazonaws.com" NEXT_PRIVATE_SMTP_PORT="587" NEXT_PRIVATE_SMTP_USERNAME="your-ses-smtp-username" NEXT_PRIVATE_SMTP_PASSWORD="your-ses-smtp-password" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` **Postmark:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="smtp.postmarkapp.com" NEXT_PRIVATE_SMTP_PORT="587" NEXT_PRIVATE_SMTP_USERNAME="your-postmark-server-api-token" NEXT_PRIVATE_SMTP_PASSWORD="your-postmark-server-api-token" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` ### SMTP with API Key (smtp-api) [#smtp-with-api-key-smtp-api] Some providers use API key authentication instead of username/password. Use this transport for services like SendGrid. **Environment Variables:** | Variable | Description | Default | | ------------------------------- | --------------------- | -------- | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Set to `smtp-api` | | | `NEXT_PRIVATE_SMTP_HOST` | SMTP server hostname | | | `NEXT_PRIVATE_SMTP_PORT` | SMTP server port | `587` | | `NEXT_PRIVATE_SMTP_APIKEY_USER` | API key username | `apikey` | | `NEXT_PRIVATE_SMTP_APIKEY` | API key value | | | `NEXT_PRIVATE_SMTP_SECURE` | Use TLS on connection | `false` | **SendGrid Example:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-api" NEXT_PRIVATE_SMTP_HOST="smtp.sendgrid.net" NEXT_PRIVATE_SMTP_PORT="587" NEXT_PRIVATE_SMTP_APIKEY_USER="apikey" NEXT_PRIVATE_SMTP_APIKEY="SG.xxxxxxxxxxxxxxxxxxxx" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` ## Resend Configuration [#resend-configuration] [Resend](https://resend.com) provides a developer-friendly email API with excellent deliverability. **Environment Variables:** | Variable | Description | | ----------------------------- | ------------------- | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Set to `resend` | | `NEXT_PRIVATE_RESEND_API_KEY` | Your Resend API key | **Example Configuration:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="resend" NEXT_PRIVATE_RESEND_API_KEY="re_xxxxxxxxxxxxxxxxxxxx" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` **Setup Steps:** {/* prettier-ignore */} ### Create an account [#create-an-account] Sign up at [resend.com](https://resend.com). ### Add and verify your sending domain [#add-and-verify-your-sending-domain] Configure your domain in the Resend dashboard so you can send from your own address. ### Create an API key [#create-an-api-key] Generate an API key in the Resend dashboard for Documenso to use. ### Configure environment variables [#configure-environment-variables] Set the variables in the table above, including `NEXT_PRIVATE_RESEND_API_KEY` and your sender address. ## MailChannels Configuration [#mailchannels-configuration] [MailChannels](https://www.mailchannels.com) is an email delivery service often used with Cloudflare Workers. **Environment Variables:** | Variable | Description | Default | | -------------------------------------------- | -------------------------------------- | ----------------------------------------- | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Set to `mailchannels` | | | `NEXT_PRIVATE_MAILCHANNELS_API_KEY` | API key for authentication | | | `NEXT_PRIVATE_MAILCHANNELS_ENDPOINT` | Custom API endpoint (for proxy setups) | `https://api.mailchannels.net/tx/v1/send` | | `NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN` | Domain for DKIM signing | | | `NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR` | DKIM selector | | | `NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY` | DKIM private key for signing | | **Example Configuration:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="mailchannels" NEXT_PRIVATE_MAILCHANNELS_API_KEY="your-api-key" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` **With DKIM Signing:** ```bash NEXT_PRIVATE_SMTP_TRANSPORT="mailchannels" NEXT_PRIVATE_MAILCHANNELS_API_KEY="your-api-key" NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN="yourdomain.com" NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR="mailchannels" NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@yourdomain.com" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" ``` ## Email Templates [#email-templates] Documenso uses React Email templates for all outgoing emails. The templates are located in `packages/email/templates/` and include: | Template | Purpose | | ------------------------- | ----------------------------------------- | | `document-invite.tsx` | Signing invitation sent to recipients | | `document-completed.tsx` | Notification when all parties have signed | | `document-pending.tsx` | Reminder for pending signatures | | `document-cancel.tsx` | Notification when a document is cancelled | | `document-rejected.tsx` | Notification when a recipient rejects | | `confirm-email.tsx` | Email verification for new accounts | | `forgot-password.tsx` | Password reset request | | `reset-password.tsx` | Password reset confirmation | | `organisation-invite.tsx` | Team/organisation invitation | ### Branding Customization [#branding-customization] Email templates support branding customization through organisation settings. When branding is enabled: * Custom logo replaces the Documenso logo * Branding colors are applied to email elements Configure branding in the application under **Organisation Settings > Preferences > Branding**. Template modifications require rebuilding the application. For simple customizations, use the branding settings instead. ## Testing Email Delivery [#testing-email-delivery] ### Verify Configuration [#verify-configuration] Test your email configuration by creating an account or resetting a password. These actions trigger emails that confirm delivery is working. ### Using a Test SMTP Server [#using-a-test-smtp-server] For development or testing, use a local SMTP server like [Inbucket](https://www.inbucket.org/), [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). The default development setup (`docker/development/compose.yml`) already runs Inbucket, with its web UI on port 9000 and SMTP on port 2500. To run one standalone instead: ```bash # Using Docker docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog ``` Configure Documenso to use the test server: ```bash NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="localhost" NEXT_PRIVATE_SMTP_PORT="1025" NEXT_PRIVATE_SMTP_FROM_ADDRESS="test@localhost" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso Test" ``` View sent emails at `http://localhost:8025`. ### Checking Logs [#checking-logs] Email sending errors appear in the application logs. Check container or server logs for messages containing `mailer` or `email`: ```bash # Docker docker logs documenso 2>&1 | grep -i email # Docker Compose docker compose logs app 2>&1 | grep -i email ``` ## Email Deliverability [#email-deliverability] ### SPF Configuration [#spf-configuration] SPF (Sender Policy Framework) authorizes servers to send email for your domain. Add a TXT record to your DNS: ``` v=spf1 include:_spf.example.com ~all ``` Replace `_spf.example.com` with your email provider's SPF include. Common examples: | Provider | SPF Include | | ------------- | ------------------------------------ | | Gmail | `include:_spf.google.com` | | Microsoft 365 | `include:spf.protection.outlook.com` | | SendGrid | `include:sendgrid.net` | | Amazon SES | `include:amazonses.com` | | Resend | `include:_spf.resend.com` | | Postmark | `include:spf.mtasv.net` | ### DKIM Configuration [#dkim-configuration] DKIM (DomainKeys Identified Mail) adds a digital signature to emails. Configuration varies by provider: {/* prettier-ignore */} ### Generate DKIM keys [#generate-dkim-keys] Create DKIM keys in your email provider's dashboard. ### Add the TXT record to DNS [#add-the-txt-record-to-dns] Add the provided TXT record to your DNS. ### Enable DKIM signing [#enable-dkim-signing] Enable DKIM signing in your provider settings. For MailChannels, configure DKIM directly in Documenso using the environment variables described above. ### DMARC Configuration [#dmarc-configuration] DMARC (Domain-based Message Authentication, Reporting, and Conformance) tells receiving servers how to handle emails that fail SPF or DKIM checks. Add a TXT record: ``` _dmarc.yourdomain.com TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com" ``` DMARC policies: | Policy | Behavior | | -------------- | ------------------------------ | | `p=none` | Monitor only, no action taken | | `p=quarantine` | Move failing emails to spam | | `p=reject` | Reject failing emails entirely | Start with `p=none` to monitor, then move to stricter policies once deliverability is confirmed. ## Common Issues [#common-issues] **Symptoms:** No emails received, no errors in logs. **Possible causes:** * Incorrect SMTP credentials * Firewall blocking outbound SMTP ports * From address not authorized **Solutions:** * Verify credentials with your email provider * Check that ports 25, 465, or 587 are open outbound * Verify the from address is authorized to send **Symptoms:** Error logs show connection timeout to SMTP server. **Possible causes:** * Incorrect hostname or port * Firewall blocking connection * SMTP server down **Solutions:** * Verify hostname and port match provider documentation * Test connectivity: `telnet smtp.example.com 587` * Check provider status page **Symptoms:** Error logs show authentication failure. **Possible causes:** * Wrong username or password * Account requires app-specific password * Account security restrictions **Solutions:** * Double-check credentials * Generate an app password if using Gmail or Microsoft with 2FA * Check for account security alerts from your provider **Symptoms:** Emails deliver but land in spam folders. **Possible causes:** * Missing or incorrect SPF record * Missing DKIM signature * No DMARC policy * Poor sender reputation **Solutions:** * Configure SPF, DKIM, and DMARC as described above * Use a reputable email provider * Verify your domain with your email provider * Monitor deliverability with tools like [mail-tester.com](https://www.mail-tester.com) **Symptoms:** Errors mentioning TLS, SSL, or certificate issues. **Possible causes:** * Server requires TLS but `NEXT_PRIVATE_SMTP_SECURE` is false * Self-signed certificate on SMTP server * Port mismatch (465 for implicit TLS, 587 for STARTTLS) **Solutions:** * Set `NEXT_PRIVATE_SMTP_SECURE="true"` for port 465 * Keep `NEXT_PRIVATE_SMTP_SECURE="false"` for port 587 (STARTTLS) * As a last resort, set `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS="true"` (not recommended for production) *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference * [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up document storage * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common issues and solutions # Environment Variables ## Required Variables [#required-variables] These variables must be set for Documenso to function: | Variable | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------ | | `NEXTAUTH_SECRET` | Secret key for NextAuth.js encryption and signing. Generate with `openssl rand -base64 32` | | `NEXT_PRIVATE_ENCRYPTION_KEY` | Primary encryption key for symmetric encryption (minimum 32 characters) | | `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | Secondary encryption key for symmetric encryption (minimum 32 characters) | | `NEXT_PUBLIC_WEBAPP_URL` | Public URL of your Documenso instance (e.g., `https://sign.example.com`) | | `NEXT_PRIVATE_DATABASE_URL` | PostgreSQL connection URL with connection pooling | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Email address used as the sender for all outgoing emails | | `NEXT_PRIVATE_SMTP_FROM_NAME` | Display name for the sender of outgoing emails | *** ## Server Configuration [#server-configuration] | Variable | Description | Default | | ---------------------------------- | ------------------------------------------------------------ | -------------------------------- | | `PORT` | Port the server listens on | `3000` | | `NEXT_PRIVATE_INTERNAL_WEBAPP_URL` | Internal URL for the app to request itself (background jobs) | Same as `NEXT_PUBLIC_WEBAPP_URL` | *** ## Database Configuration [#database-configuration] Documenso requires PostgreSQL 14 or higher. | Variable | Required | Description | | ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------ | | `NEXT_PRIVATE_DATABASE_URL` | Yes | PostgreSQL connection URL. Supports connection pooling (e.g., PgBouncer) | | `NEXT_PRIVATE_DIRECT_DATABASE_URL` | When pooling | Direct PostgreSQL URL for migrations. Defaults to `NEXT_PRIVATE_DATABASE_URL` when not set | **Connection string format:** ``` postgres://user:password@host:port/database ``` For detailed database setup, see [Database Configuration](/docs/self-hosting/configuration/database). *** ## Authentication and Security [#authentication-and-security] ### Core Authentication [#core-authentication] | Variable | Required | Description | | --------------------------------------- | -------- | ------------------------------------------------------------------------- | | `NEXTAUTH_SECRET` | Yes | Secret for NextAuth.js session encryption. Must be at least 32 characters | | `NEXT_PRIVATE_ENCRYPTION_KEY` | Yes | Primary key for encrypting sensitive data. Must be at least 32 characters | | `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | Yes | Secondary encryption key for key rotation. Must be at least 32 characters | ### Google OAuth [#google-oauth] | Variable | Required | Description | | ----------------------------------- | -------- | -------------------------- | | `NEXT_PRIVATE_GOOGLE_CLIENT_ID` | No | Google OAuth client ID | | `NEXT_PRIVATE_GOOGLE_CLIENT_SECRET` | No | Google OAuth client secret | Callback URL: `https:///api/auth/callback/google` ### Microsoft OAuth [#microsoft-oauth] | Variable | Required | Description | | -------------------------------------- | -------- | ---------------------------------------- | | `NEXT_PRIVATE_MICROSOFT_CLIENT_ID` | No | Microsoft/Azure AD application client ID | | `NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET` | No | Microsoft/Azure AD client secret | Callback URL: `https:///api/auth/callback/microsoft` ### Generic OIDC [#generic-oidc] | Variable | Default | Description | | ---------------------------------- | ------- | -------------------------------------------------- | | `NEXT_PRIVATE_OIDC_WELL_KNOWN` | - | OIDC provider well-known configuration URL | | `NEXT_PRIVATE_OIDC_CLIENT_ID` | - | OIDC client ID | | `NEXT_PRIVATE_OIDC_CLIENT_SECRET` | - | OIDC client secret | | `NEXT_PRIVATE_OIDC_PROVIDER_LABEL` | `OIDC` | Label displayed on the OIDC sign-in button | | `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | `false` | Skip email verification for OIDC accounts | | `NEXT_PRIVATE_OIDC_PROMPT` | `login` | OIDC prompt parameter. Set to empty string to omit | ### Webhooks [#webhooks] | Variable | Default | Description | | ---------------------------------------- | ------- | ------------------------------------------------------------------------ | | `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` | - | Comma-separated hostnames or IPs allowed to resolve to private addresses | Before delivering a webhook, Documenso checks whether the target resolves to a private or loopback address and blocks it if so. This check is best-effort and fails open. Use `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` to allow specific internal hosts, for example when delivering to a service on your own network: ```bash NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS="hooks.internal.example,10.0.0.5" ``` *** ## Email Configuration [#email-configuration] Documenso supports multiple email transports for sending notifications. ### Transport Selection [#transport-selection] | Variable | Description | Default | | ----------------------------- | --------------------------------------------------------------------- | ----------- | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Email transport: `smtp-auth`, `smtp-api`, `resend`, or `mailchannels` | `smtp-auth` | ### SMTP Authentication (smtp-auth) [#smtp-authentication-smtp-auth] | Variable | Default | Description | | ------------------------------------- | ----------- | ---------------------------------------- | | `NEXT_PRIVATE_SMTP_HOST` | `127.0.0.1` | SMTP server hostname | | `NEXT_PRIVATE_SMTP_PORT` | `587` | SMTP server port | | `NEXT_PRIVATE_SMTP_USERNAME` | - | SMTP authentication username | | `NEXT_PRIVATE_SMTP_PASSWORD` | - | SMTP authentication password | | `NEXT_PRIVATE_SMTP_SECURE` | `false` | Force TLS connection (`true` or `false`) | | `NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS` | `false` | Disable TLS entirely (not recommended) | | `NEXT_PRIVATE_SMTP_SERVICE` | - | Nodemailer service name (e.g., `gmail`) | ### SMTP API (smtp-api) [#smtp-api-smtp-api] | Variable | Default | Description | | ------------------------------- | -------- | ------------------------------------ | | `NEXT_PRIVATE_SMTP_HOST` | - | SMTP server hostname | | `NEXT_PRIVATE_SMTP_PORT` | `587` | SMTP server port | | `NEXT_PRIVATE_SMTP_APIKEY_USER` | `apikey` | API key user for SMTP authentication | | `NEXT_PRIVATE_SMTP_APIKEY` | - | API key for SMTP authentication | ### Resend [#resend] | Variable | Description | | ----------------------------- | ----------------------- | | `NEXT_PRIVATE_RESEND_API_KEY` | API key from Resend.com | ### MailChannels [#mailchannels] | Variable | Description | Default | | -------------------------------------------- | --------------------------------- | ----------------------------------------- | | `NEXT_PRIVATE_MAILCHANNELS_API_KEY` | MailChannels API key | - | | `NEXT_PRIVATE_MAILCHANNELS_ENDPOINT` | Custom API endpoint (for proxies) | `https://api.mailchannels.net/tx/v1/send` | | `NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN` | Domain for DKIM signing | - | | `NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR` | DKIM selector | - | | `NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY` | DKIM private key | - | ### Sender Configuration [#sender-configuration] | Variable | Description | | -------------------------------- | ------------------------------- | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Sender email address (required) | | `NEXT_PRIVATE_SMTP_FROM_NAME` | Sender display name (required) | For detailed email setup, see [Email Configuration](/docs/self-hosting/configuration/email). *** ## Storage Configuration [#storage-configuration] Documenso can store documents in the database or S3-compatible storage. | Variable | Description | Default | | ---------------------------------------- | ------------------------------------------- | ---------- | | `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Storage backend: `database` or `s3` | `database` | | `NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT` | Maximum upload size displayed to users (MB) | `5` | ### S3 Configuration [#s3-configuration] Required when `NEXT_PUBLIC_UPLOAD_TRANSPORT` is set to `s3`: | Variable | Description | Default | | --------------------------------------- | ---------------------------------------------- | ----------- | | `NEXT_PRIVATE_UPLOAD_BUCKET` | S3 bucket name | | | `NEXT_PRIVATE_UPLOAD_REGION` | S3 region | `us-east-1` | | `NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID` | S3 access key ID | | | `NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY` | S3 secret access key | | | `NEXT_PRIVATE_UPLOAD_ENDPOINT` | Custom S3 endpoint for S3-compatible providers | | | `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE` | Use path-style URLs instead of virtual hosts | `false` | ### CloudFront Distribution (Optional) [#cloudfront-distribution-optional] | Variable | Description | | ----------------------------------------------- | ------------------------------- | | `NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN` | CloudFront distribution domain | | `NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID` | CloudFront key pair ID | | `NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS` | CloudFront private key contents | For detailed storage setup, see [Storage Configuration](/docs/self-hosting/configuration/storage). *** ## Signing Certificate Configuration [#signing-certificate-configuration] Documenso requires a certificate to digitally sign documents. ### Transport Selection [#transport-selection-1] | Variable | Description | Default | | -------------------------------- | ------------------------------------------------ | ------- | | `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local`, `gcloud-hsm`, or `csc` | `local` | ### Local Signing [#local-signing] | Variable | Description | | ------------------------------------------ | -------------------------------------------------------------- | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to the `.p12` certificate file | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` file contents (alternative to file path) | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the certificate | ### Google Cloud HSM [#google-cloud-hsm] | Variable | Description | | -------------------------------------------------------------- | ---------------------------------------------------- | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH` | Google Cloud HSM key path | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH` | Path to the public certificate file | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS` | Base64-encoded public certificate | | `NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS` | Base64-encoded Google Cloud credentials | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH` | Path to the certificate chain file | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval | ### Cloud Signature Consortium (CSC) [#cloud-signature-consortium-csc] Routes signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures (AES/QES). Instance-wide; set `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` to enable. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup walkthrough. CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Without a valid license, the instance will refuse to start in `csc` mode. | Variable | Description | Default | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------- | | `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | | | `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | | | `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | | | `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller doesn't specify one. `AES` or `QES`. Explicit requests pass through. | `AES` | The OAuth callback URL registered with the CSC provider is fixed at `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` — register this exact URL with the TSP. #### Derived Public Variables [#derived-public-variables] The following client-visible variable is **derived automatically** from the private transport at server startup. Do not set it manually — any value set in the environment is overwritten on boot. | Variable | Derived from | Value | | -------------------------------------- | ------------------------------------------ | ------------------------------------------------ | | `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` | `NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc'` | `'true'` when CSC mode is active, else `'false'` | The authoring UI uses this flag to gate features that AES/QES envelopes cannot support (parallel signing, assistant role, dictate next signer). Deriving it from the private transport prevents the client-side flag from drifting from the real server-side configuration. ### Signature Options [#signature-options] | Variable | Description | Default | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures. Optional for `local` / `gcloud-hsm` (signatures omit the timestamp when unset). **Required** when `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` — the instance refuses to start without it. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes#timestamp-authority-resolution). | | | `NEXT_PUBLIC_SIGNING_CONTACT_INFO` | Contact info embedded in PDF signatures | Webapp URL | | `NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER` | Use `adbe.pkcs7.detached` instead of `ETSI.CAdES.detached` | `false` | For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/configuration/signing-certificate). *** ## Feature Flags [#feature-flags] | Variable | Description | Default | | ------------------------------------------- | --------------------------------------------------------------------------------------- | ------- | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch. Disable all signup methods application-wide | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only. SSO signup is unaffected | `false` | | `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google. Existing Google-linked users can still sign in | `false` | | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | | | `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch. Disable all signin methods application-wide | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin. Also closes `/forgot-password` and `/reset-password` | `false` | | `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable the automatic `/signin` redirect when OIDC is the only enabled transport | `false` | | `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | | | `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` | ### Signup Restrictions [#signup-restrictions] You can control who is allowed to create accounts on your instance with the following environment variables: * **`NEXT_PUBLIC_DISABLE_SIGNUP`** (master switch): Set to `true` to block all new signups across every method (email/password, Google, Microsoft, OIDC). When set, this also blocks new-account creation through the organisation OIDC authentication portal. * **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`**: Set to `true` to disable email/password signup only. SSO signup is still allowed. * **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`**: Set to `true` to block brand-new account creation through the matching SSO provider. Existing users with the provider already linked can still sign in, and existing users can still link the provider to their account. `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` also blocks new-account creation through the organisation authentication portal. * **`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`**: Restrict signups to specific email domains. When set, only users whose email address matches one of the listed domains can create an account. Leave empty to allow all domains. Sign-in for existing users is never affected, only the creation of brand-new accounts. Both the master switch and the domain allowlist apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error. When both the master switch and the domain allowlist are set, the master switch takes precedence. Signups are blocked regardless of the domain list. ```bash # Allow signups only from specific domains NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" # Allow OIDC signup only; block email/password, Google, Microsoft NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true" NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true" NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true" # Or disable signups entirely NEXT_PUBLIC_DISABLE_SIGNUP="true" ``` ### Sign-in Restrictions [#sign-in-restrictions] You can control which methods are available for users to sign in with the following environment variables: * **`NEXT_PUBLIC_DISABLE_SIGNIN`** (master switch): Set to `true` to block all signin methods (email/password, Google, Microsoft, OIDC). Hides every signin entry point on `/signin` and rejects email/password signin server-side with a `SIGNIN_DISABLED` error. * **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN`**: Set to `true` to disable email/password signin only. The email/password form is hidden, the `/forgot-password` and `/reset-password` pages redirect to `/signin`, and the corresponding server endpoints reject requests. SSO signin is unaffected. * **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNIN`**: Set to `true` to hide the matching SSO button on the signin page. Useful when an SSO provider is kept configured for account linking but not advertised as a signin entry point. These flags are opt-in: when none are set, signin behaviour is unchanged from a stock Documenso instance. ```bash # Allow only OIDC signin (e.g. enterprise SSO-only) NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" # Or disable signin entirely NEXT_PUBLIC_DISABLE_SIGNIN="true" ``` ### OIDC Auto-redirect [#oidc-auto-redirect] When OIDC is the only enabled signin transport on your instance, `/signin` automatically redirects users straight to the OIDC provider instead of showing the signin form. The page renders a spinner while the redirect happens. No extra configuration is required — disabling every other signin method is enough to trigger it. * **`NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT`**: Set to `true` to opt out of the automatic redirect and keep rendering the signin page even when OIDC is the only enabled transport. The redirect only triggers when OIDC is configured and email/password, Google, and Microsoft signin are all disabled. If any other transport remains enabled, the signin form is shown as normal. ```bash # OIDC-only signin: disabling all other methods auto-redirects to the provider NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" # Opt out of the auto-redirect while still OIDC-only # NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true" ``` *** ## AI Features [#ai-features] Documenso can use Google Vertex AI for recipient and field detection. | Variable | Description | Default | | -------------------------- | ---------------------------------------------- | -------- | | `GOOGLE_VERTEX_PROJECT_ID` | Google Cloud project ID with Vertex AI enabled | | | `GOOGLE_VERTEX_API_KEY` | Vertex AI Express API key | | | `GOOGLE_VERTEX_LOCATION` | Vertex AI region | `global` | AI features must also be enabled in organisation/team settings after configuration. *** ## Document Conversion [#document-conversion] Documenso can accept `.docx` uploads by sending them to a [Gotenberg](https://gotenberg.dev) service that converts them to PDF. When `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is unset, DOCX uploads are rejected and only PDFs are accepted. | Variable | Description | Default | | --------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------- | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` | Base URL of the Gotenberg service (e.g., `http://gotenberg:3000`). Unset disables the feature. | | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME` | HTTP Basic auth username. Required when Gotenberg runs with `--api-enable-basic-auth`. | | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD` | HTTP Basic auth password. Set together with the username. | | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` | Per-request timeout in milliseconds. Increase for very large documents. | `30000` | The public flag `NEXT_PUBLIC_DOCUMENT_CONVERSION_ENABLED` is derived automatically from `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` on server start. Do not set it manually. For setup, image-build instructions, and security recommendations, see [Document Conversion](/docs/self-hosting/configuration/advanced/document-conversion). *** ## Background Jobs [#background-jobs] Documenso supports multiple background job providers for processing emails, documents, webhooks, and scheduled tasks. ### Provider Selection [#provider-selection] | Variable | Description | Default | | ---------------------------- | ------------------------------------------------------------------------------------- | ------- | | `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (PostgreSQL), `bullmq` (Redis), or `inngest` (managed service) | `local` | ### Local (local) [#local-local] No additional configuration required. Jobs are stored in PostgreSQL and processed via internal HTTP requests. | Variable | Description | Default | | ---------------------------------- | ------------------------------------------------------- | -------------------------------- | | `NEXT_PRIVATE_INTERNAL_WEBAPP_URL` | Internal URL for the app to send job requests to itself | Same as `NEXT_PUBLIC_WEBAPP_URL` | ### BullMQ (bullmq) [#bullmq-bullmq] | Variable | Required | Description | Default | | --------------------------------- | -------- | ------------------------------------------------------------- | ----------- | | `NEXT_PRIVATE_REDIS_URL` | Yes | Redis connection URL (e.g., `redis://localhost:6379`) | | | `NEXT_PRIVATE_REDIS_PREFIX` | No | Key prefix for Redis queues (useful when sharing an instance) | `documenso` | | `NEXT_PRIVATE_BULLMQ_CONCURRENCY` | No | Number of concurrent jobs to process | `10` | ### Inngest (inngest) [#inngest-inngest] | Variable | Required | Description | | -------------------------------- | -------- | -------------------------------------------- | | `NEXT_PRIVATE_INNGEST_EVENT_KEY` | Yes | Inngest event key | | `INNGEST_EVENT_KEY` | No | Alternative Inngest event key | | `INNGEST_SIGNING_KEY` | Yes | Inngest signing key for webhook verification | | `NEXT_PRIVATE_INNGEST_APP_ID` | No | Custom Inngest app ID | For setup guides and provider recommendations, see [Background Jobs](/docs/self-hosting/configuration/background-jobs). *** ## Telemetry [#telemetry] | Variable | Description | Default | | ----------------------------- | -------------------------------------------- | ------- | | `DOCUMENSO_DISABLE_TELEMETRY` | Set to `true` to disable anonymous telemetry | `false` | Telemetry also auto-disables when `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` is configured. Telemetry collects only: app version, installation ID, and node ID. No personal data is collected. *** ## Debugging and Logging [#debugging-and-logging] | Variable | Description | | ------------------------------------------ | -------------------------------------------------- | | `NEXT_PRIVATE_LOGGER_FILE_PATH` | File path for log output. Disables stdout when set | | `NEXT_PRIVATE_BROWSERLESS_URL` | Browserless.io URL for PDF generation | | `NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS` | Use internal webapp URL in browserless requests | *** ## Enterprise Features [#enterprise-features] These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup. | Variable | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it | | `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing | | `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret | | `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification | | `NEXT_PRIVATE_SES_SECRET_ACCESS_KEY` | AWS SES secret key | | `NEXT_PRIVATE_SES_REGION` | AWS SES region | *** ## Example .env File [#example-env-file] A minimal production configuration: ```bash # Required NEXTAUTH_SECRET="your-random-secret-at-least-32-chars" NEXT_PRIVATE_ENCRYPTION_KEY="your-encryption-key-at-least-32-chars" NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="your-secondary-key-at-least-32-chars" NEXT_PUBLIC_WEBAPP_URL="https://sign.example.com" # Database NEXT_PRIVATE_DATABASE_URL="postgres://user:password@localhost:5432/documenso" NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://user:password@localhost:5432/documenso" # Email NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" NEXT_PRIVATE_SMTP_HOST="smtp.example.com" NEXT_PRIVATE_SMTP_PORT=587 NEXT_PRIVATE_SMTP_USERNAME="your-smtp-user" NEXT_PRIVATE_SMTP_PASSWORD="your-smtp-password" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@example.com" # Signing (certificate must be configured) NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" # Signup restrictions (optional) # NEXT_PUBLIC_DISABLE_SIGNUP="true" # NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true" # NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true" # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true" # NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true" # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" # Sign-in restrictions (optional) # NEXT_PUBLIC_DISABLE_SIGNIN="true" # NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" # NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" # NEXT_PUBLIC_DISABLE_OIDC_SIGNIN="true" # Opt out of the automatic OIDC redirect when OIDC is the only enabled transport (optional) # NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true" ``` *** ## See Also [#see-also] * [Database Configuration](/docs/self-hosting/configuration/database) - Set up PostgreSQL * [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery * [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing * [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Set per-organisation document, email, and API limits from the admin panel * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common configuration issues # Configuration ## Required Configuration [#required-configuration] At minimum, you need to configure: 1. **Database** - PostgreSQL connection 2. **Email** - SMTP for sending notifications 3. **Base URL** - Your instance's public URL *** ## See Also [#see-also] * [Quick Start](/docs/self-hosting/getting-started/quick-start) - Get running first, configure later * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common configuration issues # Apply Your License Key A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance. The license key applies to your **whole instance**, not an individual user account. There's one key per deployment. ## Prerequisites [#prerequisites] * An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com). See [Enterprise Edition](/docs/policies/enterprise-edition) for details. * A running self-hosted Documenso instance that you're able to restart ## Step 1: Set the environment variable [#step-1-set-the-environment-variable] Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key. Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`): ```bash NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here ``` Then apply it: ```bash docker compose up -d ``` ```bash docker run -d \ --name documenso \ -e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \ documenso/documenso:latest ``` If you're running Documenso directly (not in a container), add the variable to your `.env` file: ```bash NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here ``` ## Step 2: Restart the instance [#step-2-restart-the-instance] The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts. ```bash # Docker Compose docker compose restart documenso # Docker docker restart documenso ``` On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out. ## What the license enables [#what-the-license-enables] A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature: * **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup. * **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active. ## Troubleshooting [#troubleshooting] * Confirm the key is present in the environment the running process actually reads — `docker exec` into the container and check `env | grep LICENSE` if unsure. * Confirm the instance was fully restarted after the variable was set, not just reloaded. * Re-copy the key to rule out truncation or accidental whitespace. Instance-wide features (like CSC signing) also need their own configuration — an active license alone isn't enough. Check that feature's guide to confirm the required settings are in place. Per-organisation features additionally need to be provisioned for the organisation that's using them. ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference * [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license * [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing # Organisation Limits Per-organisation limits — document, email, and API usage, plus feature toggles and team/member caps — are controlled by **subscription claims**. You configure them in the admin panel, not through environment variables. There are three distinct kinds of limit: | Limit | Caps | Admin-settable | | ---------------------- | -------------------------------------------------- | ------------------------------------ | | Resource quota | Documents, emails, and API requests **per month** | Yes — per claim and org | | Resource rate limit | The same resources over a short window (e.g. `1h`) | Yes — per claim and org | | Global HTTP rate limit | API requests per IP (1000/min, hardcoded) | No — see [Limitations](#limitations) | ## Prerequisites [#prerequisites] * A running self-hosted Documenso instance. * An account with the **`ADMIN`** role — an account-level role, separate from organisation and team roles. New accounts are created with the `USER` role only. Grant the first admin by adding `ADMIN` to that user's `roles` directly in the database; after that, an existing admin can grant the role to others under **Admin Panel > Users > *(user)* > Roles > Update user**. Open the admin panel at `/admin`. The sidebar sections used below are **Claims**, **Organisations**, and **Organisation Stats**. ## Viewing usage [#viewing-usage] **One organisation:** open **Admin Panel > Organisations** and select it. The **Organisation usage** section shows the current period's document, email, and API usage against its quotas. **All organisations:** open **Admin Panel > Organisation Stats** to sort and filter monthly usage. Filter by **claim** and by **period** (a UTC calendar month, shown as `YYYY-MM`), and switch between **Show usage**, **Show usage with quotas**, and **Show daily averages**. Usage counts **attempts**, not only successful actions. A request that exceeds a quota is still counted before it is rejected, so displayed usage can read higher than the number of actions that succeeded. ## Subscription claims [#subscription-claims] A subscription claim is a named bundle of limits and feature flags (for example `Free`, `Individual`, `Teams`, `Platform`, or `Enterprise`). Claims are **templates**: when an organisation is created it receives a private copy of its claim and reads from that copy afterwards. Editing a claim template therefore affects organisations created later, not existing ones — to change an existing organisation, [edit it directly](#change-limits-for-one-organisation). ### Claim fields [#claim-fields] Under **Admin Panel > Claims** (`/admin/claims`), each claim has: | Field | Controls | | ----------------------- | ----------------------------------------------------------------------------------- | | **Name** | The claim's display name. | | **Team Count** | Teams allowed. `0` = unlimited. | | **Member Count** | Members allowed. `0` = unlimited. | | **Envelope Item Count** | Uploaded files allowed per envelope. Minimum `1`. | | **Recipient Count** | Recipients allowed per document. `0` = unlimited. | | **Feature Flags** | Feature toggles (see [Feature flags](#feature-flags)). | | **Limits** | Monthly quota and rate-limit windows for Documents, Emails, and API. | | **Email transport** | Transport the claim uses. *Default (system mailer)* uses the instance default. | ### Quotas and rate limits [#quotas-and-rate-limits] The **Limits** section has a column for **Documents**, **Emails**, and **API**, each with two controls: * **Monthly quota** — how many of that resource are allowed per calendar month. An **empty** field is unlimited; **`0`** blocks the resource entirely. * **Rate limit windows** — optional short-window caps, each a duration and a maximum. A window is a number and a unit (`s`, `m`, `h`, `d`), such as `5m`, `1h`, or `24h`, and must be unique within the resource. Quotas and counts use opposite conventions for "unlimited": an **empty** quota is unlimited (and `0` blocks the resource), whereas `0` in the **Team**, **Member**, and **Recipient Count** fields means unlimited. ### Feature flags [#feature-flags] The **Feature Flags** section toggles capabilities such as Unlimited documents, Branding, Hide Documenso branding, Email domains, Embed authoring, Embed signing, White label for embed authoring/signing, 21 CFR, HIPAA, Authentication portal, Allow Legacy Envelopes, Signing reminders, QES signing, and Disable emails. Some flags are Enterprise features. If your license does not include one, it is marked and cannot be enabled (you can still turn it off). See [Enterprise Edition](/docs/policies/enterprise-edition). ### Create or edit a claim template [#create-or-edit-a-claim-template] 1. Go to **Admin Panel > Claims**. 2. Select **New claim**, or select an existing claim to edit it. 3. Set the counts, feature flags, and the **Limits** section. 4. Save. Changes apply to organisations created afterwards, not existing ones. ### Change limits for one organisation [#change-limits-for-one-organisation] To change limits for an existing organisation, edit it directly rather than its claim template. 1. Go to **Admin Panel > Organisations** and open the organisation. 2. Adjust its quota, rate-limit, feature-flag, or email-transport fields. 3. Save. Changes take effect immediately. The organisation also shows the **Inherited subscription claim** it was created from. ## Usage reset [#usage-reset] Monthly quota usage is keyed to the **UTC calendar month**. There is no scheduled reset job — when the month rolls over, the new period's counter starts at `0`. ## Limitations [#limitations] The **global HTTP rate limit is not configurable.** Documenso enforces a hardcoded **1000 requests per minute per IP address** on its API endpoint groups (`/api/v1`, `/api/v2`, and the tRPC API are limited separately), returning `429 Too Many Requests`. It is a per-IP safeguard applied at the HTTP layer — not per-organisation, not stored on any claim, and not adjustable from the admin panel. See [Rate Limits](/docs/developers/api/rate-limits). ## Troubleshooting [#troubleshooting] | Symptom | Cause and fix | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | An organisation hit its limit unexpectedly | Usage counts rejected over-quota attempts. Compare usage against the quota under **Organisation Stats > Show usage with quotas**. | | A resource is blocked entirely, not just capped | The **Monthly quota** is `0`, which blocks the resource. Leave it empty for unlimited. | | Emails are not sending for an organisation | Check whether the **Disable emails** flag is enabled on the organisation's claim — it blocks all emails regardless of quota. | | A claim template edit had no effect | Template edits are not retroactive. Edit the organisation directly under **Admin Panel > Organisations**. | *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options * [Rate Limits](/docs/developers/api/rate-limits) - The global HTTP API rate limit (separate from claims) * [Enterprise Edition](/docs/policies/enterprise-edition) - Features unlocked by license flags # Storage Configuration ## Storage Options [#storage-options] | Backend | Best For | Scalability | Configuration | | ------------ | --------------------------------------- | ----------- | ------------- | | `database` | Small deployments, simplicity | Limited | None required | | `s3` | Production, large files, backups | High | Required | | `azure-blob` | Production on Azure, native Blob access | High | Required | Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable: ```bash # Database storage (default) NEXT_PUBLIC_UPLOAD_TRANSPORT=database # S3-compatible storage NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 # Azure Blob Storage (native) NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob ``` *** ## Database Storage [#database-storage] Database storage is the default option and requires no additional configuration. Documents are stored as base64-encoded data directly in PostgreSQL. * No external dependencies * Simple deployment * Automatic backups with database * Increases database size significantly * Slower for large files * Database backup/restore takes longer * Not recommended for files larger than 10MB ### Configuration [#configuration] No configuration required. Database storage is enabled when `NEXT_PUBLIC_UPLOAD_TRANSPORT` is unset or set to `database`. *** ## S3 Configuration [#s3-configuration] S3 storage is recommended for production deployments. Documenso supports AWS S3 and any S3-compatible storage service. ### Required Variables [#required-variables] | Variable | Description | | --------------------------------------- | --------------------------------- | | `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `s3` | | `NEXT_PRIVATE_UPLOAD_BUCKET` | S3 bucket name | | `NEXT_PRIVATE_UPLOAD_REGION` | AWS region (default: `us-east-1`) | | `NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID` | AWS access key ID | | `NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY` | AWS secret access key | ### Optional Variables [#optional-variables] | Variable | Description | Default | | -------------------------------------- | --------------------------------------------- | ----------- | | `NEXT_PRIVATE_UPLOAD_ENDPOINT` | Custom S3 endpoint for S3-compatible services | | | `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE` | Use path-style URLs instead of virtual-hosted | `false` | | `NEXT_PRIVATE_UPLOAD_REGION` | S3 region | `us-east-1` | *** ## AWS S3 Setup [#aws-s3-setup] {/* prettier-ignore */} ### Create an S3 Bucket [#create-an-s3-bucket] Create a bucket in the AWS Console or using the CLI: ```bash aws s3 mb s3://your-documenso-bucket --region us-east-1 ``` ### Configure Bucket Policy [#configure-bucket-policy] Block public access and configure CORS for presigned URL uploads: **CORS Configuration:** ```json [ { "AllowedHeaders": ["*"], "AllowedMethods": ["GET", "PUT", "POST"], "AllowedOrigins": ["https://your-documenso-domain.com"], "ExposeHeaders": ["ETag"] } ] ``` Apply via AWS Console (Bucket > Permissions > CORS configuration) or CLI: ```bash aws s3api put-bucket-cors --bucket your-documenso-bucket --cors-configuration file://cors.json ``` ### Create IAM User [#create-iam-user] Create an IAM user with programmatic access and attach this policy: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::your-documenso-bucket/*" } ] } ``` ### Configure Environment Variables [#configure-environment-variables] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=your-documenso-bucket NEXT_PRIVATE_UPLOAD_REGION=us-east-1 NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY ``` *** ## MinIO Setup [#minio-setup] MinIO is a self-hosted S3-compatible object storage server. {/* prettier-ignore */} ### Deploy MinIO [#deploy-minio] Using Docker: ```bash docker run -d \ --name minio \ -p 9000:9000 \ -p 9001:9001 \ -e MINIO_ROOT_USER=minioadmin \ -e MINIO_ROOT_PASSWORD=minioadmin \ -v minio_data:/data \ minio/minio server /data --console-address ":9001" ``` Using Docker Compose with Documenso: ```yaml services: minio: image: minio/minio command: server /data --console-address ":9001" ports: - '9000:9000' - '9001:9001' environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - minio_data:/data volumes: minio_data: ``` ### Create a Bucket [#create-a-bucket] Access the MinIO Console at `http://localhost:9001` and create a bucket, or use the CLI: ```bash # Install MinIO client mc alias set myminio http://localhost:9000 minioadmin minioadmin # Create bucket mc mb myminio/documenso ``` ### Configure Environment Variables [#configure-environment-variables-1] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=documenso NEXT_PRIVATE_UPLOAD_ENDPOINT=http://minio:9000 NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=minioadmin NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=minioadmin NEXT_PRIVATE_UPLOAD_REGION=us-east-1 ``` Set `NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true` for MinIO and other S3-compatible services that don't support virtual-hosted bucket URLs. *** ## Other S3-Compatible Services [#other-s3-compatible-services] Documenso works with any S3-compatible storage service. Configure the endpoint and enable path-style URLs if required. ### Cloudflare R2 [#cloudflare-r2] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=documenso NEXT_PRIVATE_UPLOAD_ENDPOINT=https://.r2.cloudflarestorage.com NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-r2-access-key NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-r2-secret-key NEXT_PRIVATE_UPLOAD_REGION=auto ``` ### DigitalOcean Spaces [#digitalocean-spaces] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=documenso NEXT_PRIVATE_UPLOAD_ENDPOINT=https://nyc3.digitaloceanspaces.com NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-spaces-key NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-spaces-secret NEXT_PRIVATE_UPLOAD_REGION=nyc3 ``` ### Backblaze B2 [#backblaze-b2] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=documenso NEXT_PRIVATE_UPLOAD_ENDPOINT=https://s3.us-west-004.backblazeb2.com NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-b2-key-id NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-b2-application-key NEXT_PRIVATE_UPLOAD_REGION=us-west-004 ``` ### Wasabi [#wasabi] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=documenso NEXT_PRIVATE_UPLOAD_ENDPOINT=https://s3.us-east-1.wasabisys.com NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-wasabi-key NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-wasabi-secret NEXT_PRIVATE_UPLOAD_REGION=us-east-1 ``` *** ## Azure Blob Storage [#azure-blob-storage] Azure Blob Storage is supported as a native transport (not S3-compatible). Documenso uses the official `@azure/storage-blob` SDK and signs SAS URLs with the Storage Account key for browser uploads and downloads. ### Required Variables [#required-variables-1] | Variable | Description | | ---------------------------------------- | --------------------------------------- | | `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `azure-blob` | | `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME` | Azure Storage Account name | | `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY` | Azure Storage Account access key | | `NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER` | Container name where uploads are stored | ### Optional Variables [#optional-variables-1] | Variable | Description | Default | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT` | Custom Blob endpoint URL. Useful for local development against Azurite (for example `http://127.0.0.1:10000`). | `https://.blob.core.windows.net` | ### Azure Setup [#azure-setup] {/* prettier-ignore */} ### Create a Storage Account and Container [#create-a-storage-account-and-container] Create a Storage Account in the Azure Portal or via the Azure CLI, then create a container inside it: ```bash az storage account create \ --name yourstorageaccount \ --resource-group your-rg \ --location eastus \ --sku Standard_LRS az storage container create \ --name documenso-documents \ --account-name yourstorageaccount ``` ### Configure CORS on the container [#configure-cors-on-the-container] The browser uploads documents directly to Azure Blob using a SAS URL, and downloads them the same way, so the Storage Account needs CORS rules that allow your application origin: ```bash az storage cors add \ --services b \ --methods GET PUT \ --origins https://your-documenso-domain.com \ --allowed-headers "Content-Type" "x-ms-blob-type" "Authorization" \ --exposed-headers "*" \ --max-age 3600 \ --account-name yourstorageaccount ``` ### Configure Environment Variables [#configure-environment-variables-2] ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=yourstorageaccount NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=your-account-key NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents ``` ### Local Development with Azurite [#local-development-with-azurite] Azurite is the official Azure Storage emulator. It supports the Blob REST API with account-key authentication. ```bash docker run -d --name azurite \ -p 10000:10000 -p 10001:10001 -p 10002:10002 \ mcr.microsoft.com/azure-storage/azurite ``` Create the container against the well-known development account: ```bash az storage container create \ --name documenso-documents \ --connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" ``` Configure environment variables to point at the emulator: ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=devstoreaccount1 NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT=http://127.0.0.1:10000 ``` The Azurite key shown above is the public well-known development key, published by Microsoft for emulator use. Never reuse it in production. *** ## CloudFront CDN (Optional) [#cloudfront-cdn-optional] Use Amazon CloudFront to serve documents with lower latency and reduced S3 costs. CloudFront integration uses signed URLs for secure access. ### Prerequisites [#prerequisites] * An S3 bucket configured for Documenso * A CloudFront distribution with the S3 bucket as origin * A CloudFront key pair for signing URLs {/* prettier-ignore */} ### Create a CloudFront Distribution [#create-a-cloudfront-distribution] * Go to CloudFront in the AWS Console * Create a distribution with your S3 bucket as the origin * Configure Origin Access Control (OAC) to restrict direct S3 access * Set the default cache behavior to allow GET requests ### Create a Key Pair [#create-a-key-pair] CloudFront signed URLs require a key pair: * Go to CloudFront > Key management > Public keys * Create a new public key * Create a key group containing the public key * Associate the key group with your distribution Keep the private key secure - you'll need it for the environment variable. ### Configure Environment Variables [#configure-environment-variables-3] ```bash # CloudFront distribution domain (without https://) NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN=d1234567890.cloudfront.net # CloudFront key pair ID NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID=K1234567890ABC # Private key contents (PEM format) NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS="-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA... -----END RSA PRIVATE KEY-----" ``` Store the private key securely. Use environment variables or secrets management rather than committing it to version control. ### How It Works [#how-it-works] When CloudFront is configured: {/* prettier-ignore */} ### Uploads [#uploads] File uploads still go directly to S3 via presigned URLs. ### Downloads [#downloads] File downloads use CloudFront signed URLs. ### Caching [#caching] CloudFront caches files at edge locations. ### Expiration [#expiration] Signed URLs expire after 1 hour. *** ## Migration Between Storage Backends [#migration-between-storage-backends] Documenso does not provide automatic migration between storage backends. Each document's storage location is recorded in the database. Documents uploaded to one storage backend cannot be automatically migrated to another. Plan your storage strategy before deploying to production. ### Manual Migration Process [#manual-migration-process] To migrate existing documents from database to S3 storage: {/* prettier-ignore */} ### Export documents [#export-documents] Extract document blobs from the database (e.g. via a script querying `DocumentData` where `type` is `BYTES_64`). ### Upload to S3 [#upload-to-s3] Upload each exported file to your S3 bucket and note the resulting object keys or paths. ### Update DocumentData records [#update-documentdata-records] Point each record to the new S3 location by updating `DocumentData` with the S3 path and setting `type` to `S3_PATH`. This requires custom scripts and database modifications. For production deployments, we recommend starting with S3 storage from the beginning. ### Hybrid Operation [#hybrid-operation] During migration, Documenso can read from both backends. The `DocumentData.type` field indicates where each document is stored. * `BYTES_64`: Stored in database * `S3_PATH`: Stored in S3 New uploads use the configured `NEXT_PUBLIC_UPLOAD_TRANSPORT` backend. *** ## Storage Sizing [#storage-sizing] ### Database Storage Estimates [#database-storage-estimates] When using database storage, plan for significant database growth: | Documents/Month | Avg Size | Monthly Growth | Annual Growth | | --------------- | -------- | -------------- | ------------- | | 100 | 500KB | \~50MB | \~600MB | | 1,000 | 500KB | \~500MB | \~6GB | | 10,000 | 500KB | \~5GB | \~60GB | Database storage includes base64 encoding overhead (\~33% increase). ### S3 Storage Estimates [#s3-storage-estimates] S3 stores files without encoding overhead: | Documents/Month | Avg Size | Monthly Growth | Annual Growth | | --------------- | -------- | -------------- | ------------- | | 100 | 500KB | \~50MB | \~600MB | | 1,000 | 500KB | \~500MB | \~6GB | | 10,000 | 500KB | \~5GB | \~60GB | ### Cost Comparison [#cost-comparison] For high-volume deployments, S3 is more cost-effective: | Aspect | Database Storage | S3 Storage | | ------------- | ----------------------------- | ------------------------ | | Storage cost | Database pricing (\~$0.10/GB) | S3 pricing (\~$0.023/GB) | | Transfer cost | Database I/O | S3 requests + egress | | Backup cost | Larger database backups | Separate S3 backups | | Performance | Degrades with size | Consistent | *** ## Upload Size Limits [#upload-size-limits] Configure the maximum upload size displayed to users: ```bash NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=10 ``` This value is in megabytes. The default is 5MB. This environment variable controls the UI display. Actual limits may also be enforced by your reverse proxy, web server, or S3 configuration. Ensure your infrastructure supports the configured limit: Set `client_max_body_size` to match or exceed your upload limit. Default object size limit is 5GB; multipart upload may be required for large files. Default limit is 50MB per request. *** ## Troubleshooting [#troubleshooting] Causes: * Incorrect IAM credentials * Bucket policy doesn't allow required operations * CORS not configured for presigned URL uploads Solutions: * Verify IAM user has `s3:PutObject` permission * Check bucket policy allows writes * Configure CORS to allow your domain Causes: * Incorrect region configuration * Bucket doesn't exist * Network connectivity issues Solutions: * Verify the bucket exists in the specified region * Check the endpoint URL is correct * Verify network access to S3 Causes: * Incorrect secret access key * Clock skew between server and S3 Solutions: * Verify credentials are correct and not expired * Ensure server time is synchronized (use NTP) Set path-style access for S3-compatible services: ```bash NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true ``` Causes: * Key pair not configured correctly * Private key format invalid Solutions: * Verify the key pair ID matches the CloudFront configuration * Ensure the private key is in PEM format * Check for whitespace or encoding issues in the key contents *** ## See Also [#see-also] * [Database Configuration](/docs/self-hosting/configuration/database) - Configure PostgreSQL * [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference * [Backups](/docs/self-hosting/maintenance/backups) - Backup strategies for both storage backends * [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Deploy with MinIO for local storage # Telemetry ## Overview [#overview] Documenso collects anonymous telemetry data from self-hosted instances to help us understand how the software is being used and make improvements to the product. This telemetry is enabled by default, but you can easily disable it if you prefer. ## What We Collect [#what-we-collect] We collect minimal, privacy-preserving information that helps us understand the health and adoption of self-hosted installations: * **App Version**: The version of Documenso you are running. This helps us understand which versions are in use and prioritize support for older versions. * **Installation ID**: A unique identifier for your installation. This is stored in your database and helps us count distinct installations without knowing who you are. * **Node ID**: A unique identifier for each server or container instance. This is stored in your operating system's temporary directory and helps us understand deployment patterns (for example, how many instances are running in a cluster). ### What We Don't Collect [#what-we-dont-collect] We do **not** collect any of the following: * Personal information about you or your users * Document contents or file names * User email addresses or names * Usage patterns or feature usage statistics * Server logs or error messages * Any data that could identify your organisation or users ## Why We Collect Telemetry [#why-we-collect-telemetry] The telemetry data we collect serves several important purposes: Understanding which versions are in use helps us prioritize bug fixes and security updates for the versions that matter most. Knowing how many installations exist and their deployment patterns helps us plan support resources and documentation. Understanding deployment patterns (like cluster sizes) helps us make better architectural decisions for future features. Tracking adoption helps us understand the growth of the self-hosted community and allocate resources accordingly. All of this is done anonymously and in aggregate. We cannot identify you, your organisation, or your users from the telemetry data we collect. ## Events We Track [#events-we-track] We track two simple events: Captured once when your server starts. This tells us when installations are first set up or restarted. Captured every hour while your server is running. This helps us understand how many active installations exist and their uptime patterns. ## How to Disable Telemetry [#how-to-disable-telemetry] If you prefer not to send telemetry data, you can disable it by setting an environment variable. ### Using Environment Variables [#using-environment-variables] Add the following to your environment configuration: ```bash DOCUMENSO_DISABLE_TELEMETRY=true ``` ### Docker [#docker] If you're using Docker, you can set this in your `docker-compose.yml`: ```yaml services: app: environment: - DOCUMENSO_DISABLE_TELEMETRY=true ``` Or pass it when running a container: ```bash docker run -e DOCUMENSO_DISABLE_TELEMETRY=true ... ``` ### After Disabling [#after-disabling] Once you set `DOCUMENSO_DISABLE_TELEMETRY=true` and restart your server, no telemetry data will be sent. The telemetry client will not initialize, and no network requests will be made to our telemetry servers. If you previously had telemetry enabled, the installation ID stored in your database will remain, but it will no longer be used or sent anywhere. ## Questions or Concerns [#questions-or-concerns] If you have questions about our telemetry practices or concerns about privacy, please reach out to us. We're committed to transparency and respect your choice to disable telemetry if you prefer. *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options * [Quick Start](/docs/self-hosting/getting-started/quick-start) - Get started with self-hosting # Docker Compose ## Prerequisites [#prerequisites] Before starting, ensure you have: * [Docker](https://docs.docker.com/get-docker/) 20.10 or later * [Docker Compose](https://docs.docker.com/compose/install/) v2.0 or later * SMTP credentials for sending emails * At least 2GB of available RAM * A domain name (for production deployments) Verify your installation: ```bash docker --version docker compose version ``` ## Clone and Configure [#clone-and-configure] {/* prettier-ignore */} ### Download the compose file [#download-the-compose-file] Download the production Docker Compose file: ```bash mkdir documenso && cd documenso curl -O https://raw.githubusercontent.com/documenso/documenso/release/docker/production/compose.yml ``` Alternatively, clone the full repository: ```bash git clone https://github.com/documenso/documenso.git cd documenso/docker/production ``` ### Create environment file [#create-environment-file] Create a `.env` file in the same directory as `compose.yml`: ```bash touch .env ``` Add the required configuration (see [Environment Configuration](#environment-configuration) below). The `compose.yml` in the repository may be outdated. Use it as a starting point and verify the configuration against the environment variables documented here. ## Docker Compose File Overview [#docker-compose-file-overview] The production `compose.yml` includes two services: ```yaml name: documenso-production services: database: image: postgres:15 environment: - POSTGRES_USER=${POSTGRES_USER:?err} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err} - POSTGRES_DB=${POSTGRES_DB:?err} healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] interval: 10s timeout: 5s retries: 5 volumes: - database:/var/lib/postgresql/data documenso: image: documenso/documenso:latest depends_on: database: condition: service_healthy environment: # See environment configuration below ports: - ${PORT:-3000}:${PORT:-3000} volumes: - /opt/documenso/cert.p12:/opt/documenso/cert.p12:ro volumes: database: ``` | Service | Purpose | | ----------- | ------------------------------------------------------------ | | `database` | PostgreSQL 15 database with persistent storage | | `documenso` | Main application container, waits for database to be healthy | The Documenso container waits for the database health check to pass before starting. ## Environment Configuration [#environment-configuration] Create a `.env` file with the following variables: ### Required Variables [#required-variables] ```bash # Database (used by both database and documenso services) POSTGRES_USER=documenso POSTGRES_PASSWORD=your-secure-database-password POSTGRES_DB=documenso # Application secrets (generate with: openssl rand -base64 32) NEXTAUTH_SECRET=your-nextauth-secret NEXT_PRIVATE_ENCRYPTION_KEY=your-encryption-key-min-32-characters NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=your-secondary-key-min-32-characters # Public URL where Documenso is accessible NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000 # Database connection (uses Docker service name) NEXT_PRIVATE_DATABASE_URL=postgresql://documenso:your-secure-database-password@database:5432/documenso # Email configuration NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth NEXT_PRIVATE_SMTP_HOST=smtp.example.com NEXT_PRIVATE_SMTP_PORT=587 NEXT_PRIVATE_SMTP_USERNAME=your-smtp-username NEXT_PRIVATE_SMTP_PASSWORD=your-smtp-password NEXT_PRIVATE_SMTP_FROM_NAME=Documenso NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@example.com ``` ### Optional Variables [#optional-variables] ```bash # Application port (default: 3000) PORT=3000 # Signing certificate (see Signing Certificate section) NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password # Signup restrictions (optional) # Master switch — disables every signup method NEXT_PUBLIC_DISABLE_SIGNUP=false # Per-method switches (optional). Each disables brand-new account creation through that method. # NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true # NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=true # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true # NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org # Signin restrictions (optional) # Master switch — disables every signin method # NEXT_PUBLIC_DISABLE_SIGNIN=true # Per-method switches (optional). Each disables that signin path. # NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true # NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=true # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=true # NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=true # When OIDC is the only enabled transport, /signin auto-redirects to the provider. # Set this to opt out and keep showing the signin page (optional). # NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true ``` Generate secure secrets using: `openssl rand -base64 32` For the complete list of environment variables, see [Environment Variables](/docs/self-hosting/configuration/environment). ### Generating Secrets [#generating-secrets] Generate the required secrets: ```bash # Generate NEXTAUTH_SECRET echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" # Generate encryption keys echo "NEXT_PRIVATE_ENCRYPTION_KEY=$(openssl rand -base64 32)" echo "NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=$(openssl rand -base64 32)" # Generate database password echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" ``` ## Signing Certificate [#signing-certificate] A signing certificate is required for document signing. Generate a `.p12` certificate on your host machine and mount it into the container. See [Local Certificate](/docs/self-hosting/configuration/signing-certificate/local) for how to generate one. Place the certificate on the host and set permissions so the container can read it (UID 1001): ```bash sudo mkdir -p /opt/documenso sudo cp /path/to/your/cert.p12 /opt/documenso/cert.p12 sudo chown 1001:1001 /opt/documenso/cert.p12 sudo chmod 400 /opt/documenso/cert.p12 ``` The `compose.yml` mounts this path into the container. Add the passphrase to your `.env` file: ```bash NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password ``` If file mounting is not available, you can set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` with the base64-encoded certificate string instead. For production deployments that require Adobe Approved Trust List recognition, consider using a [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) or another external HSM. Do not generate or store the signing certificate inside the container. If the container is destroyed and rebuilt, or if you run multiple instances, the certificate will be lost or inconsistent. Always provide the certificate externally. ## Starting Services [#starting-services] Start all services: ```bash docker compose --env-file .env up -d ``` Check that containers are running: ```bash docker compose ps ``` Expected output: ``` NAME STATUS PORTS documenso-production-database-1 running (healthy) 5432/tcp documenso-production-documenso-1 running 0.0.0.0:3000->3000/tcp ``` Wait for the database to be healthy and for migrations to complete. Check the logs: ```bash docker compose logs -f documenso ``` Look for "Ready" or "Listening on port 3000" in the output. ## Accessing Documenso [#accessing-documenso] Once the containers are running: * **Local access**: Open [http://localhost:3000](http://localhost:3000) * **Remote access**: Use the URL configured in `NEXT_PUBLIC_WEBAPP_URL` ### First Account Setup [#first-account-setup] Navigate to the signup page and create your account. Verify your email address — if emails are not being delivered, check the container logs for SMTP errors. All accounts created through signup are regular user accounts. Admin access must be granted directly in the database. Once your accounts are set up, consider disabling public signups by setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`. For finer control, use the per-method switches `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`, `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`, `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`, `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`, or restrict signups to specific email domains with `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`. ## Managing Services [#managing-services] ### View Logs [#view-logs] View all service logs: ```bash docker compose logs -f ``` View logs for a specific service: ```bash docker compose logs -f documenso docker compose logs -f database ``` ### Restart Services [#restart-services] Restart all services: ```bash docker compose --env-file .env restart ``` Restart a specific service: ```bash docker compose --env-file .env restart documenso ``` ### Stop Services [#stop-services] Stop without removing containers: ```bash docker compose stop ``` Stop and remove containers (preserves volumes): ```bash docker compose down ``` Stop, remove containers, and delete data: ```bash docker compose down -v ``` Using `down -v` deletes the database volume. Back up your data first. ### Update Documenso [#update-documenso] {/* prettier-ignore */} Pull the latest image: ```bash docker compose pull ``` Recreate containers: ```bash docker compose --env-file .env up -d ``` Database migrations run automatically on container startup. Back up your database before upgrading. See [Backups](/docs/self-hosting/maintenance/backups). ## Production Considerations [#production-considerations] ### Reverse Proxy [#reverse-proxy] For production, place Documenso behind a reverse proxy for SSL termination: **nginx example:** ```nginx server { listen 443 ssl http2; server_name sign.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` **Caddy example:** ``` sign.example.com { reverse_proxy localhost:3000 } ``` ### Database Backups [#database-backups] Set up automated database backups: ```bash # Manual backup docker compose exec database pg_dump -U documenso documenso > backup.sql # Restore from backup docker compose exec -T database psql -U documenso documenso < backup.sql ``` See [Backups](/docs/self-hosting/maintenance/backups) for automated backup strategies. ### Resource Limits [#resource-limits] Add resource limits to prevent container resource exhaustion: ```yaml services: documenso: deploy: resources: limits: cpus: '2' memory: 2G reservations: cpus: '0.5' memory: 512M ``` ### External Database [#external-database] For production, consider using a managed PostgreSQL service: {/* prettier-ignore */} Remove the `database` service from `compose.yml` Update environment variables: ```bash NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@your-db-host:5432/documenso NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@your-db-host:5432/documenso ``` ### S3 Storage [#s3-storage] For high-volume deployments, configure S3-compatible storage: ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=your-bucket NEXT_PRIVATE_UPLOAD_REGION=us-east-1 NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-access-key NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-secret-key ``` See [Storage Configuration](/docs/self-hosting/configuration/storage). ## Troubleshooting [#troubleshooting] Check logs: `docker compose logs documenso`. Common causes: missing environment variables (ensure all required variables are in `.env`), database not ready (the container waits for database health check), port conflict (change `PORT` in `.env` if 3000 is in use). Verify the database is healthy: `docker compose ps database`. Test: `docker compose exec database psql -U documenso -d documenso -c "SELECT 1"`. Check certificate status: `curl http://localhost:3000/api/certificate-status`. Common issues: file not found (verify volume mount in `compose.yml`), permission denied (run `chown 1001:1001` on the certificate file), no password (the certificate must have a password). Check `NEXT_PRIVATE_SMTP_TRANSPORT` matches your setup, verify host, port, username, and password, check logs: `docker compose logs documenso | grep -i smtp`. Verify containers are running: `docker compose ps`; check the port mapping matches your `.env`; test locally: `curl http://localhost:3000/api/health`; check firewall rules allow traffic on the configured port. Check disk usage: `docker system df`. Clean up: `docker system prune -a`. *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Certificate setup details * [Email Configuration](/docs/self-hosting/configuration/email) - SMTP and email provider setup * [Storage Configuration](/docs/self-hosting/configuration/storage) - S3 storage setup * [Backups](/docs/self-hosting/maintenance/backups) - Backup strategies * [Upgrades](/docs/self-hosting/maintenance/upgrades) - Upgrade procedures # Docker ## Prerequisites [#prerequisites] For a self-contained setup that includes PostgreSQL, see [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose). Before starting, ensure you have: * [Docker](https://docs.docker.com/get-docker/) 20.10 or later installed * A PostgreSQL 14+ database accessible from your Docker host * SMTP credentials for sending emails * At least 1GB of available RAM Verify Docker is installed: ```bash docker --version ``` ## Pulling the Docker Image [#pulling-the-docker-image] The Documenso image is available on both DockerHub and GitHub Container Registry: ```bash # DockerHub docker pull documenso/documenso:latest # GitHub Container Registry docker pull ghcr.io/documenso/documenso:latest ``` ### Available Tags [#available-tags] | Tag | Description | | --------- | -------------------------------- | | `latest` | Latest stable release | | `x.y.z` | Specific version (e.g., `1.5.0`) | | `release` | Latest release branch build | Pin to a specific version tag in production to avoid unexpected updates. ## Environment Variables [#environment-variables] ### Required Variables [#required-variables] These variables must be set for Documenso to start: | Variable | Description | Default | | --------------------------------------- | --------------------------------------------------------------------------- | -------------------------------- | | `NEXTAUTH_SECRET` | Secret key for session encryption. Generate with `openssl rand -base64 32` | - | | `NEXT_PRIVATE_ENCRYPTION_KEY` | Primary encryption key (minimum 32 characters) | - | | `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | Secondary encryption key (minimum 32 characters) | - | | `NEXT_PUBLIC_WEBAPP_URL` | Public URL where Documenso is accessible (e.g., `https://sign.example.com`) | - | | `NEXT_PRIVATE_INTERNAL_WEBAPP_URL` | Internal URL the app uses to call itself (for background jobs) | Same as `NEXT_PUBLIC_WEBAPP_URL` | | `NEXT_PRIVATE_DATABASE_URL` | PostgreSQL connection string | - | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Email transport: `smtp-auth`, `smtp-api`, `resend`, or `mailchannels` | `smtp-auth` | | `NEXT_PRIVATE_SMTP_FROM_NAME` | Sender name for outgoing emails | - | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Sender email address | - | ### Database Connection [#database-connection] Documenso requires two database connection strings: | Variable | Required | Default | Description | | ---------------------------------- | ------------ | --------------------------- | --------------------------------- | | `NEXT_PRIVATE_DATABASE_URL` | Yes | - | Pooled connection for app queries | | `NEXT_PRIVATE_DIRECT_DATABASE_URL` | When pooling | `NEXT_PRIVATE_DATABASE_URL` | Direct connection for migrations | Connection string format: ``` postgresql://user:password@host:5432/database ``` If you're not using a connection pooler like PgBouncer, both variables can use the same connection string. ### SMTP Configuration [#smtp-configuration] For `smtp-auth` transport: | Variable | Required | Default | Description | | ---------------------------- | -------- | ----------- | ------------------------------------- | | `NEXT_PRIVATE_SMTP_HOST` | Yes | `127.0.0.1` | SMTP server hostname | | `NEXT_PRIVATE_SMTP_PORT` | Yes | `587` | SMTP server port (usually 587 or 465) | | `NEXT_PRIVATE_SMTP_USERNAME` | Yes | - | SMTP username | | `NEXT_PRIVATE_SMTP_PASSWORD` | Yes | - | SMTP password | | `NEXT_PRIVATE_SMTP_SECURE` | No | `false` | Set to `true` to force TLS | See [Email Configuration](/docs/self-hosting/configuration/email) for other transport options. ### Optional Variables [#optional-variables] | Variable | Description | Default | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `PORT` | Port the application listens on | `3000` | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to signing certificate inside container — set to the volume-mount path (e.g. `/opt/documenso/cert.p12`). Only Docker Compose defaults this; plain `docker run` must set it explicitly | - | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - | | `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` | | `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` | | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | | `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` | | `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` | For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment). ## Running with Docker [#running-with-docker] ### Minimum Example [#minimum-example] ```bash docker run -d \ --name documenso \ -p 3000:3000 \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ -e NEXTAUTH_SECRET="$(openssl rand -base64 32)" \ -e NEXT_PRIVATE_ENCRYPTION_KEY="your-encryption-key-min-32-chars" \ -e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="your-secondary-key-min-32-chars" \ -e NEXT_PUBLIC_WEBAPP_URL="https://sign.example.com" \ -e NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000" \ -e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@db-host:5432/documenso" \ -e NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH="/opt/documenso/cert.p12" \ -e NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" \ -e NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" \ -e NEXT_PRIVATE_SMTP_HOST="smtp.example.com" \ -e NEXT_PRIVATE_SMTP_PORT="587" \ -e NEXT_PRIVATE_SMTP_USERNAME="your-smtp-user" \ -e NEXT_PRIVATE_SMTP_PASSWORD="your-smtp-password" \ -e NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" \ -e NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@example.com" \ documenso/documenso:latest ``` ### Signing Certificate [#signing-certificate] A signing certificate is required for document signing. You have two options for providing one: * **Volume mount** — mount a `.p12` file from the host into the container at `/opt/documenso/cert.p12` (shown above). This is the simplest approach for small to moderate deployments. * **Base64-encoded contents** — set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` with the base64-encoded certificate string. Use this when file mounting is not available (e.g., Railway, Vercel). Plain `docker run` deployments must set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` explicitly. This prevents production deployments from accidentally using the insecure example certificate. Docker Compose sets the file path for you. For production deployments that require Adobe Approved Trust List recognition, consider using a [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) or another external HSM. Do not generate or store the signing certificate inside the container. If the container is destroyed and rebuilt, or if you run multiple instances, the certificate will be lost or inconsistent. Always provide the certificate externally. See [Signing Certificate Configuration](/docs/self-hosting/configuration/signing-certificate) for generating certificates and detailed setup. ### Using an Environment File [#using-an-environment-file] For easier management, use an environment file: Create `.env`: ```bash NEXTAUTH_SECRET=your-secret-here NEXT_PRIVATE_ENCRYPTION_KEY=your-encryption-key-min-32-chars NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=your-secondary-key-min-32-chars NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000 NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@db-host:5432/documenso NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@db-host:5432/documenso NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth NEXT_PRIVATE_SMTP_HOST=smtp.example.com NEXT_PRIVATE_SMTP_PORT=587 NEXT_PRIVATE_SMTP_USERNAME=your-smtp-user NEXT_PRIVATE_SMTP_PASSWORD=your-smtp-password NEXT_PRIVATE_SMTP_FROM_NAME=Documenso NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@example.com ``` Run with the environment file: ```bash docker run -d \ --name documenso \ -p 3000:3000 \ --env-file .env \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ documenso/documenso:latest ``` ## Health Checks [#health-checks] Documenso provides health check endpoints for monitoring: If a certificate is mounted but signing fails, ensure `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` explicitly points to its path inside the container. Production does not use the development example certificate as a fallback. | Endpoint | Purpose | | ------------------------- | -------------------------------------------------------------- | | `/api/health` | Checks database connectivity and certificate status | | `/api/certificate-status` | Returns whether a signing certificate is configured and usable | Both endpoints return a JSON response with a `status` field: | Status | Meaning | | ----------- | ------------------------------------------------------------------- | | `"ok"` | Everything is working properly | | `"warning"` | Application is running but there are certificate issues | | `"error"` | Critical issues (database unreachable, missing configuration, etc.) | ### Docker Health Check [#docker-health-check] Add a health check to your container: ```bash docker run -d \ --name documenso \ -p 3000:3000 \ --health-cmd="curl -f http://localhost:3000/api/health || exit 1" \ --health-interval=30s \ --health-timeout=10s \ --health-retries=3 \ --env-file .env \ documenso/documenso:latest ``` Check container health status: ```bash docker inspect --format='{{.State.Health.Status}}' documenso ``` ## Updating the Container [#updating-the-container] To update Documenso to a new version: {/* prettier-ignore */} ### Pull the new image [#pull-the-new-image] ```bash docker pull documenso/documenso:latest ``` ### Stop and remove the existing container [#stop-and-remove-the-existing-container] ```bash docker stop documenso docker rm documenso ``` ### Start a new container with the same configuration [#start-a-new-container-with-the-same-configuration] ```bash docker run -d \ --name documenso \ -p 3000:3000 \ --env-file .env \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ documenso/documenso:latest ``` Database migrations run automatically when the container starts. Back up your database before upgrading. ## Persistence [#persistence] The Documenso container is stateless. All persistent data is stored in the PostgreSQL database. By default, documents are also stored in the database. For high-volume deployments, configure S3-compatible storage instead. See [Storage Configuration](/docs/self-hosting/configuration/storage) for S3 setup. ## Troubleshooting [#troubleshooting] Check the container logs: `docker logs documenso`. Common causes: missing required environment variables (ensure all required variables are set), database connection failed (verify the database URL and network connectivity), port already in use (change the host port or stop the conflicting service). Test connectivity: `docker run --rm postgres:15 psql "postgresql://user:password@host:5432/database" -c "SELECT 1"`. If using Docker networks, ensure the container can reach the database. Check certificate status: `curl http://localhost:3000/api/certificate-status`. Common issues: file not found (verify the volume mount path), permission denied (ensure the file is readable, UID 1001 inside the container), no password (the certificate must have a password set). Fix permissions: `chmod 644 /path/to/cert.p12` and `chown 1001:1001 /path/to/cert.p12`. Verify SMTP configuration: check `NEXT_PRIVATE_SMTP_TRANSPORT` matches your configuration, for `smtp-auth` ensure host, port, username, and password are correct, check if your SMTP provider requires TLS (`NEXT_PRIVATE_SMTP_SECURE=true`). Verify port mapping `-p 3000:3000`, check if `NEXT_PUBLIC_WEBAPP_URL` matches how you're accessing the application, ensure no firewall is blocking the port. *** ## See Also [#see-also] * [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Multi-container setup with PostgreSQL included * [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing * [Email Configuration](/docs/self-hosting/configuration/email) - Configure SMTP providers * [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3-compatible storage # Deployment | Method | Best For | Difficulty | | -------------------------------------------------------------- | ----------------------------------- | ---------- | | [Docker](/docs/self-hosting/deployment/docker) | Single container, external database | Easy | | [Docker Compose](/docs/self-hosting/deployment/docker-compose) | Self-contained setup, small teams | Easy | | [Railway](/docs/self-hosting/deployment/railway) | Quick deployment, managed | Very Easy | | [Kubernetes](/docs/self-hosting/deployment/kubernetes) | Enterprise, high availability | Advanced | ## See Also [#see-also] * [Requirements](/docs/self-hosting/getting-started/requirements) - Check prerequisites first * [Configuration](/docs/self-hosting/configuration) - Configure your deployment # Kubernetes ## Prerequisites [#prerequisites] Before starting, ensure you have: * A Kubernetes cluster (1.24+) running and accessible * `kubectl` configured to communicate with your cluster * A PostgreSQL 14+ database (in-cluster or external) * SMTP credentials for sending emails * A domain name with DNS configured * An Ingress controller installed (nginx-ingress, traefik, etc.) Verify your cluster connection: ```bash kubectl cluster-info kubectl get nodes ``` ## Architecture Overview [#architecture-overview] A typical Documenso Kubernetes deployment consists of: ## Namespace Setup [#namespace-setup] Create a dedicated namespace for Documenso: ```yaml # namespace.yaml apiVersion: v1 kind: Namespace metadata: name: documenso labels: app.kubernetes.io/name: documenso ``` Apply the namespace: ```bash kubectl apply -f namespace.yaml ``` ## Secrets Configuration [#secrets-configuration] Store sensitive configuration in Kubernetes Secrets. ### Generate Secrets [#generate-secrets] First, generate the required secret values: ```bash # Generate NEXTAUTH_SECRET openssl rand -base64 32 # Generate encryption keys (minimum 32 characters each) openssl rand -base64 32 openssl rand -base64 32 ``` ### Create the Secret [#create-the-secret] ```yaml # secret.yaml apiVersion: v1 kind: Secret metadata: name: documenso-secrets namespace: documenso labels: app.kubernetes.io/name: documenso type: Opaque stringData: NEXTAUTH_SECRET: 'your-generated-secret-here' NEXT_PRIVATE_ENCRYPTION_KEY: 'your-encryption-key-min-32-chars' NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: 'your-secondary-key-min-32-chars' NEXT_PRIVATE_DATABASE_URL: 'postgresql://user:password@postgres-host:5432/documenso' NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://user:password@postgres-host:5432/documenso' NEXT_PRIVATE_SMTP_USERNAME: 'your-smtp-username' NEXT_PRIVATE_SMTP_PASSWORD: 'your-smtp-password' NEXT_PRIVATE_SIGNING_PASSPHRASE: 'your-certificate-passphrase' ``` Never commit Secret manifests with real values to version control. Use a secrets management tool like Sealed Secrets, External Secrets Operator, or your cloud provider's secret manager. Apply the secret: ```bash kubectl apply -f secret.yaml ``` ## ConfigMap [#configmap] Store non-sensitive configuration in a ConfigMap: ```yaml # configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: documenso-config namespace: documenso labels: app.kubernetes.io/name: documenso data: NEXT_PUBLIC_WEBAPP_URL: 'https://sign.example.com' NEXT_PRIVATE_SMTP_TRANSPORT: 'smtp-auth' NEXT_PRIVATE_SMTP_HOST: 'smtp.example.com' NEXT_PRIVATE_SMTP_PORT: '587' NEXT_PRIVATE_SMTP_FROM_NAME: 'Documenso' NEXT_PRIVATE_SMTP_FROM_ADDRESS: 'noreply@example.com' NEXT_PRIVATE_INTERNAL_WEBAPP_URL: 'http://localhost:3000' NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: '/opt/documenso/cert.p12' NEXT_PUBLIC_UPLOAD_TRANSPORT: 'database' ``` Apply the ConfigMap: ```bash kubectl apply -f configmap.yaml ``` ## Deployment [#deployment] Create the Documenso Deployment: ```yaml # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web spec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web spec: securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 containers: - name: documenso image: documenso/documenso:latest imagePullPolicy: Always ports: - name: http containerPort: 3000 protocol: TCP envFrom: - configMapRef: name: documenso-config - secretRef: name: documenso-secrets resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1000m memory: 1Gi livenessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 startupProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 30 volumeMounts: - name: signing-cert mountPath: /opt/documenso/cert.p12 subPath: cert.p12 readOnly: true volumes: - name: signing-cert secret: secretName: documenso-signing-cert items: - key: cert.p12 path: cert.p12 ``` Pin to a specific image tag (e.g., `documenso/documenso:`) in production instead of `latest` to ensure predictable deployments. ### Signing Certificate Secret [#signing-certificate-secret] Create a secret for the signing certificate: ```bash kubectl create secret generic documenso-signing-cert \ --namespace documenso \ --from-file=cert.p12=/path/to/your/cert.p12 ``` Apply the deployment: ```bash kubectl apply -f deployment.yaml ``` ## Service [#service] Expose the Deployment with a Service: ```yaml # service.yaml apiVersion: v1 kind: Service metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web spec: type: ClusterIP ports: - name: http port: 80 targetPort: http protocol: TCP selector: app.kubernetes.io/name: documenso app.kubernetes.io/component: web ``` Apply the service: ```bash kubectl apply -f service.yaml ``` ## Ingress Configuration [#ingress-configuration] ### nginx-ingress [#nginx-ingress] ```yaml # ingress-nginx.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso annotations: nginx.ingress.kubernetes.io/proxy-body-size: '50m' nginx.ingress.kubernetes.io/proxy-read-timeout: '300' nginx.ingress.kubernetes.io/proxy-send-timeout: '300' cert-manager.io/cluster-issuer: 'letsencrypt-prod' spec: ingressClassName: nginx tls: - hosts: - sign.example.com secretName: documenso-tls rules: - host: sign.example.com http: paths: - path: / pathType: Prefix backend: service: name: documenso port: name: http ``` ### Traefik [#traefik] ```yaml # ingress-traefik.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.tls: 'true' cert-manager.io/cluster-issuer: 'letsencrypt-prod' spec: ingressClassName: traefik tls: - hosts: - sign.example.com secretName: documenso-tls rules: - host: sign.example.com http: paths: - path: / pathType: Prefix backend: service: name: documenso port: name: http ``` Apply your chosen ingress: ```bash kubectl apply -f ingress-nginx.yaml # or kubectl apply -f ingress-traefik.yaml ``` ## Database Options [#database-options] ### External Database (Recommended) [#external-database-recommended] For production, use a managed PostgreSQL service: * AWS RDS for PostgreSQL * Google Cloud SQL * Azure Database for PostgreSQL * DigitalOcean Managed Databases Update `NEXT_PRIVATE_DATABASE_URL` in your Secret with the connection string from your provider. ### In-Cluster PostgreSQL [#in-cluster-postgresql] For testing or development, deploy PostgreSQL in the cluster: ```yaml # postgres.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-pvc namespace: documenso spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: v1 kind: Secret metadata: name: postgres-secrets namespace: documenso type: Opaque stringData: POSTGRES_USER: documenso POSTGRES_PASSWORD: your-secure-password POSTGRES_DB: documenso --- apiVersion: apps/v1 kind: Deployment metadata: name: postgres namespace: documenso spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: postgres template: metadata: labels: app.kubernetes.io/name: postgres spec: containers: - name: postgres image: postgres:15-alpine ports: - containerPort: 5432 envFrom: - secretRef: name: postgres-secrets volumeMounts: - name: postgres-data mountPath: /var/lib/postgresql/data resources: requests: cpu: 250m memory: 256Mi limits: cpu: 1000m memory: 1Gi volumes: - name: postgres-data persistentVolumeClaim: claimName: postgres-pvc --- apiVersion: v1 kind: Service metadata: name: postgres namespace: documenso spec: type: ClusterIP ports: - port: 5432 targetPort: 5432 selector: app.kubernetes.io/name: postgres ``` In-cluster PostgreSQL is not recommended for production. It lacks high availability, automated backups, and point-in-time recovery that managed services provide. Update your Documenso secret to use the in-cluster database: ```yaml NEXT_PRIVATE_DATABASE_URL: 'postgresql://documenso:your-secure-password@postgres.documenso.svc.cluster.local:5432/documenso' NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://documenso:your-secure-password@postgres.documenso.svc.cluster.local:5432/documenso' ``` ## Persistent Storage [#persistent-storage] Documenso stores documents in the database by default. For high-volume deployments, configure S3-compatible storage. ### S3 Configuration [#s3-configuration] Add these to your ConfigMap and Secret: ```yaml # In configmap.yaml, add: data: NEXT_PUBLIC_UPLOAD_TRANSPORT: 's3' # In secret.yaml, add: stringData: NEXT_PRIVATE_UPLOAD_ENDPOINT: 'https://s3.amazonaws.com' NEXT_PRIVATE_UPLOAD_REGION: 'us-east-1' NEXT_PRIVATE_UPLOAD_BUCKET: 'your-documenso-bucket' NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID: 'your-access-key' NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY: 'your-secret-key' ``` See [Storage Configuration](/docs/self-hosting/configuration/storage) for detailed S3 setup instructions. ## Scaling [#scaling] ### Horizontal Pod Autoscaler (HPA) [#horizontal-pod-autoscaler-hpa] Scale Documenso based on CPU and memory utilization: ```yaml # hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: documenso minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 10 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max ``` Apply the HPA: ```bash kubectl apply -f hpa.yaml ``` ### Pod Disruption Budget [#pod-disruption-budget] Ensure availability during cluster maintenance: ```yaml # pdb.yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: documenso namespace: documenso labels: app.kubernetes.io/name: documenso spec: minAvailable: 1 selector: matchLabels: app.kubernetes.io/name: documenso app.kubernetes.io/component: web ``` Apply the PDB: ```bash kubectl apply -f pdb.yaml ``` ## Troubleshooting [#troubleshooting] Check pod status: ```bash # Get pod status kubectl get pods -n documenso # Describe pod kubectl describe pod -n documenso ``` Common causes: * ImagePullBackOff (check image name and registry access) * CrashLoopBackOff (check logs for application errors) * Pending (check resource requests and node capacity) Verify the database is accessible from the cluster: ```bash kubectl run postgres-test --rm -it --image=postgres:15-alpine -n documenso -- psql "postgresql://user:password@host:5432/database" -c "SELECT 1" ``` Check Ingress status: ```bash # Get Ingress status kubectl get ingress -n documenso # Describe Ingress kubectl describe ingress documenso -n documenso ``` Verify the Ingress controller is running: ```bash # Get Ingress controller status kubectl get pods -n ingress-nginx # Get Traefik controller status kubectl get pods -n traefik ``` Check the signing certificate secret: ```bash # Get signing certificate secret kubectl get secret documenso-signing-cert -n documenso -o yaml ``` Verify the certificate is mounted correctly: ```bash # Execute command in pod kubectl exec -it -n documenso -- ls -la /opt/documenso/ ``` Test the health endpoint: ```bash # Port forward kubectl port-forward svc/documenso 3000:80 -n documenso # Test health endpoint curl http://localhost:3000/api/health ``` If a deployment fails, rollback: ```bash # View history kubectl rollout history deployment/documenso -n documenso # Rollback to previous version kubectl rollout undo deployment/documenso -n documenso # Rollback to a specific revision kubectl rollout undo deployment/documenso -n documenso --to-revision=2 ``` ## Complete Example [#complete-example] Here's a combined manifest for reference: ```yaml # documenso-complete.yaml --- apiVersion: v1 kind: Namespace metadata: name: documenso labels: app.kubernetes.io/name: documenso --- apiVersion: v1 kind: ConfigMap metadata: name: documenso-config namespace: documenso data: NEXT_PUBLIC_WEBAPP_URL: 'https://sign.example.com' NEXT_PRIVATE_SMTP_TRANSPORT: 'smtp-auth' NEXT_PRIVATE_SMTP_HOST: 'smtp.example.com' NEXT_PRIVATE_SMTP_PORT: '587' NEXT_PRIVATE_SMTP_FROM_NAME: 'Documenso' NEXT_PRIVATE_SMTP_FROM_ADDRESS: 'noreply@example.com' NEXT_PRIVATE_INTERNAL_WEBAPP_URL: 'http://localhost:3000' NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: '/opt/documenso/cert.p12' NEXT_PUBLIC_UPLOAD_TRANSPORT: 'database' --- apiVersion: v1 kind: Secret metadata: name: documenso-secrets namespace: documenso type: Opaque stringData: NEXTAUTH_SECRET: 'REPLACE_WITH_GENERATED_SECRET' NEXT_PRIVATE_ENCRYPTION_KEY: 'REPLACE_WITH_32_CHAR_KEY' NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: 'REPLACE_WITH_32_CHAR_KEY' NEXT_PRIVATE_DATABASE_URL: 'postgresql://user:password@host:5432/documenso' NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://user:password@host:5432/documenso' NEXT_PRIVATE_SMTP_USERNAME: 'smtp-user' NEXT_PRIVATE_SMTP_PASSWORD: 'smtp-password' NEXT_PRIVATE_SIGNING_PASSPHRASE: 'cert-passphrase' --- apiVersion: apps/v1 kind: Deployment metadata: name: documenso namespace: documenso spec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: documenso template: metadata: labels: app.kubernetes.io/name: documenso spec: securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 containers: - name: documenso image: documenso/documenso:latest ports: - name: http containerPort: 3000 envFrom: - configMapRef: name: documenso-config - secretRef: name: documenso-secrets resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1000m memory: 1Gi livenessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 startupProbe: httpGet: path: /api/health port: http initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 volumeMounts: - name: signing-cert mountPath: /opt/documenso/cert.p12 subPath: cert.p12 readOnly: true volumes: - name: signing-cert secret: secretName: documenso-signing-cert --- apiVersion: v1 kind: Service metadata: name: documenso namespace: documenso spec: type: ClusterIP ports: - name: http port: 80 targetPort: http selector: app.kubernetes.io/name: documenso --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: documenso namespace: documenso annotations: nginx.ingress.kubernetes.io/proxy-body-size: '50m' cert-manager.io/cluster-issuer: 'letsencrypt-prod' spec: ingressClassName: nginx tls: - hosts: - sign.example.com secretName: documenso-tls rules: - host: sign.example.com http: paths: - path: / pathType: Prefix backend: service: name: documenso port: name: http --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: documenso namespace: documenso spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: documenso minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: documenso namespace: documenso spec: minAvailable: 1 selector: matchLabels: app.kubernetes.io/name: documenso ``` *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing * [Storage Configuration](/docs/self-hosting/configuration/storage) - Configure S3-compatible storage * [Email Configuration](/docs/self-hosting/configuration/email) - Configure SMTP providers * [Backups](/docs/self-hosting/maintenance/backups) - Database backup strategies * [Upgrades](/docs/self-hosting/maintenance/upgrades) - Upgrade procedures # Manual Deployment This guide is a community contribution and may not always reflect the latest changes. If you encounter issues, please refer to the [Docker](/docs/self-hosting/deployment/docker) or [Docker Compose](/docs/self-hosting/deployment/docker-compose) guides which are actively maintained. ## Prerequisites [#prerequisites] * Node.js 24 or later * npm 11.17 or later * PostgreSQL 14 or later * A Linux server (for systemd service setup) ## Install and Build [#install-and-build] {/* prettier-ignore */} ### Clone the repository [#clone-the-repository] ```bash git clone https://github.com/documenso/documenso.git ``` ### Configure environment variables [#configure-environment-variables] Navigate to the `documenso` folder and create a `.env` file: ```bash cp .env.example .env ``` Open the `.env` file and configure the required variables: ```bash NEXTAUTH_SECRET="your-secret-here" NEXT_PRIVATE_ENCRYPTION_KEY="your-encryption-key-min-32-chars" NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="your-secondary-key-min-32-chars" NEXT_PUBLIC_WEBAPP_URL="https://your-domain.com" NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000" NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@localhost:5432/documenso" NEXT_PRIVATE_DIRECT_DATABASE_URL="postgresql://user:password@localhost:5432/documenso" NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@your-domain.com" ``` If you use a reverse proxy in front of Documenso, set `NEXT_PUBLIC_WEBAPP_URL` to the public URL that users will access. ### Install dependencies and build [#install-dependencies-and-build] ```bash npm ci npm run build npm run prisma:migrate-deploy ``` ### Start the application [#start-the-application] ```bash npm run start ``` The server starts on `localhost:3000` by default. To use a different port, set the `PORT` environment variable in your `.env` file: ```bash PORT=3500 ``` ## Run as a systemd Service [#run-as-a-systemd-service] Create a service file to run Documenso as a background service on Linux: ```ini # /etc/systemd/system/documenso.service [Unit] Description=Documenso After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/documenso EnvironmentFile=/var/www/documenso/.env ExecStart=/usr/bin/node apps/remix/build/server/main.js TimeoutSec=15 Restart=always [Install] WantedBy=multi-user.target ``` The `EnvironmentFile` directive loads your `.env` file, so configuration (including `PORT`) is managed in one place. Alternatively, you can set environment variables directly in the service file with `Environment=` directives. Enable and start the service: ```bash sudo systemctl enable documenso sudo systemctl start documenso ``` ## Reverse Proxy with Nginx [#reverse-proxy-with-nginx] A minimal Nginx configuration for proxying to Documenso: ```nginx server { listen 80; server_name your-domain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` For production, configure SSL termination with Let's Encrypt or your preferred certificate provider. *** ## See Also [#see-also] * [Docker Deployment](/docs/self-hosting/deployment/docker) - Deploy using a standalone Docker container * [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Deploy with Docker Compose * [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing certificates # Railway This guide is a community contribution and may be outdated. If you run into issues, check the [GitHub Discussions](https://github.com/documenso/documenso/discussions) for help. ## Prerequisites [#prerequisites] Before deploying, you need: * A [Railway account](https://railway.app/) (free tier available) * SMTP credentials for sending emails (required for document signing requests) * A custom domain (optional but recommended for production) ## One-Click Deploy [#one-click-deploy] The fastest way to deploy Documenso on Railway is using the official template: [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0\&utm_medium=integration\&utm_source=template\&utm_campaign=generic) This template automatically provisions: * Documenso application service * PostgreSQL database * Required environment variables with secure defaults {/* prettier-ignore */} ### Click the deploy button [#click-the-deploy-button] Click the "Deploy on Railway" button above. You'll be redirected to Railway and prompted to log in if needed. ### Configure environment variables [#configure-environment-variables] Railway will prompt you to configure required variables. At minimum, set: | Variable | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `NEXT_PUBLIC_WEBAPP_URL` | The public URL for your deployment (use the Railway-provided URL initially, update later if using a custom domain) | | `NEXT_PRIVATE_SMTP_HOST` | Your SMTP server hostname | | `NEXT_PRIVATE_SMTP_PORT` | SMTP port (typically 587 or 465) | | `NEXT_PRIVATE_SMTP_USERNAME` | SMTP username | | `NEXT_PRIVATE_SMTP_PASSWORD` | SMTP password | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Sender email address | | `NEXT_PRIVATE_SMTP_FROM_NAME` | Sender display name | The template automatically generates secure values for `NEXTAUTH_SECRET`, `NEXT_PRIVATE_ENCRYPTION_KEY`, and `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY`. ### Deploy [#deploy] Click "Deploy" to start the deployment. Railway builds the application from the Dockerfile and provisions the database. This takes approximately 5-10 minutes for the initial build. ### Access your deployment [#access-your-deployment] Once deployed, Railway provides a URL like `documenso-production.up.railway.app`. Click the URL in the Railway dashboard to access your Documenso instance. ## Manual Deployment [#manual-deployment] If you prefer more control over the setup, deploy Documenso manually: {/* prettier-ignore */} ### Create a new project [#create-a-new-project] * Log in to [Railway](https://railway.app/) * Click "New Project" * Select "Deploy from GitHub repo" * Connect your GitHub account if not already connected * Fork or select the [Documenso repository](https://github.com/documenso/documenso) ### Configure the build [#configure-the-build] Railway automatically detects the `railway.toml` configuration file in the repository, which specifies: ```toml [build] builder = "DOCKERFILE" dockerfilePath = "/docker/Dockerfile" ``` No additional build configuration is needed. ### Add PostgreSQL database [#add-postgresql-database] * In your project, click "New" * Select "Database" * Choose "PostgreSQL" * Railway provisions the database and automatically sets `DATABASE_URL` ### Configure environment variables [#configure-environment-variables-1] Click on the Documenso service and go to the "Variables" tab. Add the following variables: **Required variables:** ```bash NEXTAUTH_SECRET= NEXT_PRIVATE_ENCRYPTION_KEY= NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY= NEXT_PUBLIC_WEBAPP_URL=https://.up.railway.app NEXT_PRIVATE_DATABASE_URL=${{Postgres.DATABASE_URL}} NEXT_PRIVATE_DIRECT_DATABASE_URL=${{Postgres.DATABASE_URL}} NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth NEXT_PRIVATE_SMTP_HOST= NEXT_PRIVATE_SMTP_PORT=587 NEXT_PRIVATE_SMTP_USERNAME= NEXT_PRIVATE_SMTP_PASSWORD= NEXT_PRIVATE_SMTP_FROM_NAME=Documenso NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com ``` Use `${{Postgres.DATABASE_URL}}` to reference the Railway-managed PostgreSQL connection string. Railway automatically resolves this reference. **Optional variables:** | Variable | Description | Default | | ------------------------------------------- | ----------------------------------------------------------- | ------- | | `PORT` | Application port | `3000` | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` | | `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` | | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | | `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` | | `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - | | `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` | ### Deploy [#deploy-1] Click "Deploy" or push a commit to trigger a new deployment. Railway builds and deploys the application automatically. ## Environment Variables [#environment-variables] Railway manages environment variables through its dashboard. The full list of configuration options is documented in [Environment Variables](/docs/self-hosting/configuration/environment). ### Referencing Railway Services [#referencing-railway-services] Railway allows referencing other services in your project using the `${{ServiceName.VARIABLE}}` syntax: ```bash NEXT_PRIVATE_DATABASE_URL=${{Postgres.DATABASE_URL}} ``` This ensures connection strings stay in sync when Railway updates service configurations. ## Database Setup [#database-setup] Railway's managed PostgreSQL handles database provisioning automatically. The database: * Runs PostgreSQL 15 by default * Includes automatic backups on paid plans * Provides connection pooling via the internal network ### Database Connection [#database-connection] For Railway deployments, use the same connection string for both pooled and direct connections: ```bash NEXT_PRIVATE_DATABASE_URL=${{Postgres.DATABASE_URL}} NEXT_PRIVATE_DIRECT_DATABASE_URL=${{Postgres.DATABASE_URL}} ``` Documenso runs database migrations automatically on startup. ### Accessing the Database [#accessing-the-database] To connect directly to your database for debugging or maintenance: {/* prettier-ignore */} ### Open the PostgreSQL service [#open-the-postgresql-service] In your Railway project dashboard, select the PostgreSQL service. ### Open the Connect tab [#open-the-connect-tab] The "Connect" tab shows the connection string and credentials. ### Connect with your client [#connect-with-your-client] Use the provided connection string with your preferred PostgreSQL client. ## Custom Domain [#custom-domain] Railway provides a generated URL by default. To use your own domain: {/* prettier-ignore */} ### Generate a domain [#generate-a-domain] * Click on your Documenso service * Go to "Settings" > "Networking" * Click "Generate Domain" to get a Railway subdomain, or proceed to add a custom domain ### Add custom domain [#add-custom-domain] * In "Settings" > "Networking", click "Custom Domain" * Enter your domain (e.g., `sign.yourdomain.com`) * Railway provides DNS records to configure ### Configure DNS [#configure-dns] Add the following DNS records at your domain registrar: | Type | Name | Value | | ----- | ---- | ------------------------------- | | CNAME | sign | `.up.railway.app` | DNS propagation can take up to 24 hours. ### Update environment variable [#update-environment-variable] Update `NEXT_PUBLIC_WEBAPP_URL` to match your custom domain: ```bash NEXT_PUBLIC_WEBAPP_URL=https://sign.yourdomain.com ``` Railway automatically provisions and renews SSL certificates for custom domains. ## Scaling Options [#scaling-options] Railway provides several options for scaling your Documenso deployment: ### Vertical Scaling [#vertical-scaling] Increase resources for your service: {/* prettier-ignore */} ### Open your Documenso service [#open-your-documenso-service] In the Railway dashboard, select the Documenso service. ### Open Settings [#open-settings] In "Settings" you can change the resource limits for the service. ### Adjust memory and CPU [#adjust-memory-and-cpu] Raise the memory and CPU limits; Railway will scale within these limits based on demand. ### Horizontal Scaling [#horizontal-scaling] Railway supports horizontal scaling through replicas: {/* prettier-ignore */} ### Open service Settings [#open-service-settings] In the Railway dashboard, select your Documenso service and open "Settings". ### Enable Horizontal Scaling [#enable-horizontal-scaling] Turn on "Horizontal Scaling" so Railway can run multiple instances. ### Set replicas [#set-replicas] Choose how many replicas to run; Railway will load-balance traffic across them. Documenso stores sessions in the database, so horizontal scaling works without additional configuration. However, ensure your database can handle the increased connection load. ### Database Scaling [#database-scaling] For high-traffic deployments: * Upgrade to a larger PostgreSQL instance in Railway * Consider using connection pooling with PgBouncer * Configure read replicas for read-heavy workloads ## Cost Estimate [#cost-estimate] Railway uses usage-based pricing. Estimated monthly costs for Documenso: | Tier | Resources | Estimated Cost | | ----- | ---------------------------------- | -------------- | | Trial | $5 credit, limited hours | Free | | Hobby | 512MB RAM, shared CPU | $5-10/month | | Pro | 2GB RAM, dedicated CPU | $20-40/month | | Team | Custom resources, priority support | $50+/month | Actual costs depend on: * Application memory usage (typically 500MB-1GB) * Database size and query volume * Egress bandwidth * Number of deployments Railway charges based on actual resource consumption. Monitor usage in the Railway dashboard to optimize costs. ### Free Tier Limitations [#free-tier-limitations] Railway's trial tier includes: * $5 of free credit * 500 hours of execution time per month * Limited to 512MB RAM per service This is sufficient for testing but not recommended for production. ## Signing Certificate [#signing-certificate] Documenso requires a signing certificate to sign documents. On Railway, you can: ### Option 1: Use Generated Certificate (Default) [#option-1-use-generated-certificate-default] Documenso generates a self-signed certificate automatically if none is provided. This works for testing but shows generic certificate information in signed documents. ### Option 2: Mount Custom Certificate [#option-2-mount-custom-certificate] For production use, provide your own certificate: {/* prettier-ignore */} ### Base64-encode your certificate [#base64-encode-your-certificate] Run this and copy the output (single line, no newlines): ```bash base64 -i your-cert.p12 | tr -d '\n' ``` ### Add environment variables in Railway [#add-environment-variables-in-railway] In your Documenso service variables, set: ```bash NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS= NEXT_PRIVATE_SIGNING_PASSPHRASE= ``` Your certificate must have a password. Certificates without passwords cause signing errors. See [Signing Certificate Configuration](/docs/self-hosting/configuration/signing-certificate) for detailed instructions. ## Troubleshooting [#troubleshooting] Check the build logs in Railway for specific errors. Common causes: out of memory (increase the build memory limit in service settings), Docker build timeout (Railway has a 20-minute build timeout; ensure your build completes within this limit). View the deployment logs: click your service, go to Deployments, click the failed deployment. Common issues: missing environment variables (verify all required variables are set), database connection failed (check `NEXT_PRIVATE_DATABASE_URL` references the correct PostgreSQL service), invalid encryption keys (ensure keys are at least 32 characters). Verify SMTP configuration: check `NEXT_PRIVATE_SMTP_TRANSPORT` is set to `smtp-auth`, verify SMTP credentials are correct, ensure your SMTP provider allows connections from Railway's IP addresses. Verify PostgreSQL service is running, check the variable reference syntax `${{Postgres.DATABASE_URL}}`, try redeploying the application. Increase memory allocation for the service, check database query performance in Railway's metrics, consider upgrading to a larger PostgreSQL instance. ## Other One-Click Deployment Options [#other-one-click-deployment-options] ### Render [#render] [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso) ### Koyeb [#koyeb] Deploy to Koyeb *** ## See Also [#see-also] * [Email Configuration](/docs/self-hosting/configuration/email) - Configure email providers * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing * [Storage Configuration](/docs/self-hosting/configuration/storage) - Configure S3 storage for documents * [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference # Getting Started **You must generate a signing certificate.** Documenso does not ship with one. Without a certificate, the application starts normally but document signing will fail on completion with errors. Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding. # Quick Start ## Limitations [#limitations] This quick start setup is for local development and testing only: * No HTTPS (not suitable for production) * No email delivery (emails are logged to console) * Uses a local PostgreSQL database * No signing certificate configured (documents will be signed with a generated certificate) * Single-node setup (no high availability) For production deployments, see [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose). ## Prerequisites [#prerequisites] * [Docker](https://docs.docker.com/get-docker/) installed and running * [Docker Compose](https://docs.docker.com/compose/install/) v2.0 or later * At least 2GB of available RAM Verify your installation: ```bash docker --version docker compose version ``` ## Quick Start [#quick-start] {/* prettier-ignore */} ### Clone the repository [#clone-the-repository] ```bash git clone https://github.com/documenso/documenso.git cd documenso ``` ### Create the environment file [#create-the-environment-file] Copy the example environment file: ```bash cp .env.example .env ``` The default values work for local development. No changes are required. ### Start Documenso [#start-documenso] ```bash docker compose -f docker/development/compose.yml up -d ``` This pulls the required images and starts the containers. The first run takes a few minutes. ### Access the application [#access-the-application] Open [http://localhost:3000](http://localhost:3000) in your browser. ## Create Your First Account [#create-your-first-account] {/* prettier-ignore */} ### Sign up [#sign-up] Click **Sign Up** on the login page and enter your email address and password. ### Verify your email [#verify-your-email] In development mode, emails are logged to the console instead of being sent. Check the Docker logs for the verification link: ```bash docker compose -f docker/development/compose.yml logs -f ``` Copy the verification link from the logs and open it in your browser. ### Log in [#log-in] Log in with your credentials. ### Granting Admin Access [#granting-admin-access] All accounts created through signup are regular user accounts. To grant admin access, update the user's role directly in the database: ```bash docker compose -f docker/development/compose.yml exec database \ psql -U documenso -d documenso -c "UPDATE \"User\" SET roles = '{ADMIN}' WHERE email = 'your@email.com';" ``` ## What's Included [#whats-included] The quick start setup runs the following containers: | Container | Purpose | Port | | ----------- | ------------------------------ | -------------------------- | | `documenso` | Main application | 3000 | | `database` | PostgreSQL database | 54320 | | `inbucket` | Local email testing server | 9000 (web UI), 2500 (SMTP) | | `redis` | Cache and background job queue | 63790 | | `minio` | S3-compatible storage | 9002 (API), 9001 (console) | | `gotenberg` | Document conversion (optional) | 3005 | The local email server is [Inbucket](https://www.inbucket.org/). Open its web UI at [http://localhost:9000](http://localhost:9000) to view emails Documenso sends during development. For your own deployment you can use any SMTP-compatible mailserver, such as Inbucket, [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). ## Useful Commands [#useful-commands] **View logs:** ```bash docker compose -f docker/development/compose.yml logs -f ``` **Stop containers:** ```bash docker compose -f docker/development/compose.yml down ``` **Stop and remove all data:** ```bash docker compose -f docker/development/compose.yml down -v ``` **Rebuild after changes:** ```bash docker compose -f docker/development/compose.yml up -d --build ``` ## Troubleshooting [#troubleshooting] Another application is using port 3000. Either stop that application or change the port in your `.env` file: `PORT=3001`. Then restart the containers. Check the logs: `docker compose -f docker/development/compose.yml logs documenso`. Common issues: insufficient memory (ensure Docker has at least 2GB RAM allocated), database connection (wait for the database container to be healthy before the app starts). Verify containers are running: `docker compose -f docker/development/compose.yml ps`. Check if the app container is healthy. On Linux, ensure Docker is not running in rootless mode with network restrictions. *** ## See Also [#see-also] * [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Production setup with PostgreSQL and email * [Environment Variables](/docs/self-hosting/configuration/environment) - Configure all available options * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing * [Email Configuration](/docs/self-hosting/configuration/email) - Configure SMTP for production # Requirements ## What You Need [#what-you-need] Documenso requires the following items and external services: | Service | Purpose | Minimum Version | | ------------------- | ------------------------------- | --------------- | | Signing certificate | Digital signature for documents | N/A | | PostgreSQL | Primary database | 14+ | | SMTP server | Sending emails to recipients | Any | | Reverse proxy | SSL termination, routing | Any | ### Signing Certificate [#signing-certificate] Documenso does not ship with a signing certificate. Without one, the application starts normally but all document signing will fail. You must generate or provide a `.p12` certificate before going to production. Every completed document is digitally signed using an X.509 certificate. You can generate a self-signed certificate for free or use one from a Certificate Authority (CA). * [Generate a local certificate](/docs/self-hosting/configuration/signing-certificate/local) — step-by-step instructions to create a `.p12` certificate * [All certificate options](/docs/self-hosting/configuration/signing-certificate) — self-signed, CA-issued, and Google Cloud HSM ### PostgreSQL Database [#postgresql-database] Documenso uses PostgreSQL for all data storage including documents, users, and audit logs. You cannot use MySQL, SQLite, or other databases. Documenso requires two connection strings: * **Pooled connection** (`NEXT_PRIVATE_DATABASE_URL`) - For general application queries * **Direct connection** (`NEXT_PRIVATE_DIRECT_DATABASE_URL`) - For migrations and operations that require a direct connection If you're not using a connection pooler (like PgBouncer), both can point to the same database URL. ### Email Server [#email-server] Documenso sends transactional emails for: * Document signing requests * Signing reminders * Completion notifications * Password resets and verification Supported transports: | Transport | Use Case | | -------------- | ------------------------------------ | | `smtp-auth` | Standard SMTP with username/password | | `smtp-api` | SMTP with API key authentication | | `resend` | Resend.com API | | `mailchannels` | MailChannels API | Without a working email configuration, recipients cannot receive signing requests. Configure email before going to production. ### Reverse Proxy [#reverse-proxy] For production deployments, place Documenso behind a reverse proxy such as: * nginx * Caddy * Traefik * HAProxy * Cloud load balancers (AWS ALB, GCP Load Balancer, etc.) The reverse proxy handles SSL/TLS termination and forwards requests to Documenso on port 3000. *** ## Optional Services [#optional-services] These services are not required but improve functionality or scalability: | Service | Purpose | | --------------------- | ---------------------------------------- | | S3-compatible storage | Store documents externally (recommended) | ### Document Storage [#document-storage] By default, Documenso stores documents in the PostgreSQL database. For production deployments with significant document volume, use S3-compatible storage: * Amazon S3 * MinIO * Cloudflare R2 * DigitalOcean Spaces * Any S3-compatible provider See [Storage Configuration](/docs/self-hosting/configuration/storage) for setup details. ### Background Jobs [#background-jobs] Documenso processes background jobs (email delivery, document processing) using a PostgreSQL-based queue by default. No additional services are required: the job queue is built into the application and uses your existing database. For production deployments that need higher throughput or more reliable job processing, Documenso supports [BullMQ](https://docs.bullmq.io/) as an alternative provider. BullMQ requires a **Redis** instance (v6.2+). Set `NEXT_PRIVATE_JOBS_PROVIDER=bullmq` and configure `NEXT_PRIVATE_REDIS_URL`. For managed/cloud deployments, [Inngest](https://www.inngest.com/) is also supported as a job provider. Set `NEXT_PRIVATE_JOBS_PROVIDER=inngest` and configure `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`. See [Background Jobs Configuration](/docs/self-hosting/configuration/background-jobs) for full details. *** ## Hardware Requirements [#hardware-requirements] ### Minimum (Testing/Development) [#minimum-testingdevelopment] | Resource | Requirement | | -------- | ----------- | | CPU | 1 core | | RAM | 1 GB | | Storage | 10 GB | ### Recommended (Production) [#recommended-production] | Resource | Requirement | | -------- | ----------- | | CPU | 2+ cores | | RAM | 2+ GB | | Storage | 20+ GB | Storage requirements depend on document volume. If using external S3 storage, local storage needs are minimal. If storing documents in the database, plan for growth accordingly. *** ## Build Requirements [#build-requirements] If building from source (not using Docker images): | Requirement | Version | | ----------- | ------- | | Node.js | 24+ | | npm | 11.17+ | *** ## Supported Platforms [#supported-platforms] Documenso runs on: * **Linux** - Any modern distribution (Ubuntu, Debian, CentOS, Alpine) * **Docker** - Official images available on DockerHub and GitHub Container Registry * **Kubernetes** - Helm charts and manifests available * **PaaS providers** - Railway, Render, Koyeb (one-click deploys available) *** ## What's NOT Supported [#whats-not-supported] The following configurations are not supported and may not work correctly. | Configuration | Reason | | -------------- | ------------------------------------- | | Windows native | Use Docker or WSL2 instead | | MySQL/MariaDB | PostgreSQL-specific features required | | SQLite | Not suitable for production workloads | | MongoDB | Relational database required | | Node.js \< 24 | Modern JavaScript features required | *** ## Checklist [#checklist] Before proceeding to deployment, confirm you have: * [ ] PostgreSQL 14+ database accessible * [ ] SMTP credentials or email provider API key * [ ] Signing certificate (`.p12`) for document signing — see [Tips](/docs/self-hosting/getting-started/tips) * [ ] Domain name for your Documenso instance * [ ] SSL certificate (or reverse proxy that handles SSL) * [ ] Server meeting minimum hardware requirements *** ## See Also [#see-also] * [Quick Start](/docs/self-hosting/getting-started/quick-start) - Deploy Documenso with Docker in 5 minutes * [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Production setup with Docker Compose * [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference # Tips & Common Pitfalls ## Always Configure a Signing Certificate [#always-configure-a-signing-certificate] This is the single most common issue when self-hosting Documenso. Without a signing certificate, the application starts normally but document signing fails. Documenso does **not** bundle a [signing certificate](/docs/self-hosting/configuration/signing-certificate) in the Docker image. You must provide one yourself using one of two methods: ### Volume Mount (Recommended) [#volume-mount-recommended] Mount your `.p12` certificate file into the container at the default path: ```bash docker run -d \ -v /path/to/your/cert.p12:/opt/documenso/cert.p12:ro \ -e NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" \ documenso/documenso:latest ``` The default certificate path inside the container is `/opt/documenso/cert.p12`. You can change this with `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH`. ### Base64 Environment Variable [#base64-environment-variable] For platforms that don't support volume mounts (Railway, Render, etc.), encode the certificate and pass it as an environment variable: ```bash # Encode the certificate (no line breaks) base64 -w 0 certificate.p12 # Linux base64 -i certificate.p12 # macOS ``` Then set: ```bash NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS= NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password ``` `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` takes precedence over `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` if both are set. ### Verify Your Certificate [#verify-your-certificate] After starting Documenso, check the certificate status endpoint: ```bash curl http://localhost:3000/api/certificate-status ``` The startup script also checks for the certificate and prints a warning if it's missing. *** ## Certificate Password is Required [#certificate-password-is-required] Certificates without a password cause signing to fail with: ``` Error: Failed to get private key bags ``` Always set a password when creating your `.p12` certificate. When using OpenSSL, set the password during the export step and provide it to Documenso via `NEXT_PRIVATE_SIGNING_PASSPHRASE`. *** ## Use the Health Check Endpoints [#use-the-health-check-endpoints] Documenso exposes two endpoints for monitoring: | Endpoint | Purpose | | ------------------------- | ------------------------------------ | | `/api/health` | Returns 200 if the application is up | | `/api/certificate-status` | Detailed signing certificate status | Use `/api/health` for container orchestration health checks (Docker, Kubernetes). Use `/api/certificate-status` to debug signing issues. It reports whether a certificate is configured, its type, and any errors. *** ## Pin Your Docker Image Version [#pin-your-docker-image-version] Use a specific version tag in production: ```bash # Good — predictable, reproducible docker pull documenso/documenso: # Risky — may pull breaking changes docker pull documenso/documenso:latest ``` Check the [GitHub releases](https://github.com/documenso/documenso/releases) page for available versions. *** ## Migrations Run Automatically on Startup [#migrations-run-automatically-on-startup] The Docker container runs `prisma migrate deploy` every time it starts. This means: * **Upgrades are simple**: pull the new image, restart the container, and migrations apply automatically. * **Always back up your database before upgrading**: if a migration fails, you need a way to restore. * **First startup takes longer**: the initial migration creates all tables. *** ## Set Internal Webapp URL for Background Jobs [#set-internal-webapp-url-for-background-jobs] Background jobs work by Documenso sending HTTP requests to itself. If your reverse proxy or network setup causes issues with the app reaching its own public URL, set the internal URL: ```bash NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000 ``` This tells the job system to use the internal address instead of `NEXT_PUBLIC_WEBAPP_URL` for self-requests. *** ## Database Storage vs S3 [#database-storage-vs-s3] Documents are stored in PostgreSQL by default (`NEXT_PUBLIC_UPLOAD_TRANSPORT=database`). This works well for small-to-medium deployments and keeps your infrastructure simple. For high-volume deployments, switch to S3-compatible storage to keep your database lean: ```bash NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 NEXT_PRIVATE_UPLOAD_BUCKET=documenso-documents NEXT_PRIVATE_UPLOAD_REGION=us-east-1 NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=your-key NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=your-secret ``` Any S3-compatible provider works: AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces. See [Storage Configuration](/docs/self-hosting/configuration/storage) for full setup details. *** ## Background Jobs [#background-jobs] Documenso uses a PostgreSQL-based job queue by default (`local` provider). No Redis or external message broker is required for basic deployments. For production workloads, consider switching to **Inngest** (managed) or **BullMQ** (self-hosted with Redis) for better reliability and throughput. See [Background Jobs Configuration](/docs/self-hosting/configuration/background-jobs) for setup instructions and provider comparison. *** ## IPv6-Only Deployments [#ipv6-only-deployments] If you are deploying to an environment that uses only IPv6, set the `HOST` environment variable to `::` so the application binds to all IPv6 addresses: **Docker:** ```bash docker run -it -e HOST=:: documenso/documenso:latest npm run start ``` **Kubernetes or Docker Compose:** ```yaml containers: - name: documenso image: documenso/documenso:latest command: - npm args: - run - start env: - name: HOST value: '::' ``` *** ## Docker File Permissions [#docker-file-permissions] The Documenso container runs as a non-root user (UID 1001). If you mount files into the container (certificates, configuration), ensure they're readable: ```bash # On the host, before mounting chmod 644 cert.p12 chown 1001:1001 cert.p12 ``` If you see `EACCES: permission denied` errors, this is almost always the cause. *** ## See Also [#see-also] * [Requirements](/docs/self-hosting/getting-started/requirements) - What you need before deploying * [Quick Start](/docs/self-hosting/getting-started/quick-start) - Get running in 5 minutes * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Full certificate setup guide * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Detailed error resolution # Backups ## What to Back Up [#what-to-back-up] Documenso stores data in three locations: | Component | Location | Contains | | --------------------- | ----------------------------- | -------------------------------------------------- | | PostgreSQL database | Database server | Users, documents, signatures, audit logs, settings | | Signing certificate | File system or secret manager | Private key for signing documents | | Document storage (S3) | S3 bucket or database | Uploaded PDFs and signed documents | The signing certificate is critical. If lost, you cannot sign new documents until a new certificate is configured. Back it up securely. ### Data Stored in PostgreSQL [#data-stored-in-postgresql] The database contains: * User accounts and authentication data * Teams and organisation settings * Document metadata (recipients, fields, status) * Signatures and signing history * Audit logs and activity records * Templates and template settings * API tokens and webhook configurations If using database storage (`NEXT_PUBLIC_UPLOAD_TRANSPORT=database`), the database also contains the actual PDF files. *** ## Database Backups [#database-backups] ### Using pg\_dump [#using-pg_dump] Create a logical backup with `pg_dump`: | Flag | Description | | ------ | ----------------------------------------------------- | | `-h` | Database host | | `-U` | Database user | | `-d` | Database name | | `-F c` | Custom format (compressed, supports parallel restore) | | `-f` | Output file | ```bash pg_dump -h localhost -U documenso -d documenso -F c -f documenso_backup.dump ``` ```bash docker compose exec database pg_dump -U documenso -F c documenso > documenso_backup.dump ``` ### Backup with Timestamp [#backup-with-timestamp] Include timestamps in backup filenames for easier management: ```bash pg_dump -h localhost -U documenso -d documenso -F c \ -f "documenso_$(date +%Y%m%d_%H%M%S).dump" ``` ### Plain SQL Backup [#plain-sql-backup] For human-readable backups or cross-version compatibility: ```bash pg_dump -h localhost -U documenso -d documenso -F p -f documenso_backup.sql ``` ### Compressed Backup [#compressed-backup] Compress backups to save storage space: ```bash pg_dump -h localhost -U documenso -d documenso | gzip > documenso_backup.sql.gz ``` *** ## Certificate Backups [#certificate-backups] The signing certificate (`.p12` file) is required to sign documents. Store backups securely. ### Locate Your Certificate [#locate-your-certificate] Check your environment configuration for the certificate location: ```bash echo $NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH ``` ```bash echo $NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS ``` ### Back Up Certificate [#back-up-certificate] Copy the certificate to a secure backup location: ```bash cp /opt/documenso/cert.p12 /backup/documenso/cert.p12 ``` If using base64-encoded certificate contents: ```bash echo "$NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS" | base64 -d > /backup/documenso/cert.p12 ``` ### Store Certificate Passphrase [#store-certificate-passphrase] Document the certificate passphrase (`NEXT_PRIVATE_SIGNING_PASSPHRASE`) in a secure password manager or secrets vault. The certificate is unusable without it. Store certificate backups separately from database backups. Use encryption for certificate backup storage. *** ## Storage Backups (S3) [#storage-backups-s3] If using S3 storage (`NEXT_PUBLIC_UPLOAD_TRANSPORT=s3`), back up your S3 bucket. Enable versioning on your bucket: ```bash aws s3api put-bucket-versioning \ --bucket your-documenso-bucket \ --versioning-configuration Status=Enabled ``` Sync to a backup location or download locally: ```bash aws s3 sync s3://your-documenso-bucket s3://your-backup-bucket # or aws s3 sync s3://your-documenso-bucket /backup/documenso/documents/ ``` Use the same commands with the `--endpoint-url` flag: ```bash aws s3 sync s3://your-bucket /backup/documenso/documents/ \ --endpoint-url https://your-s3-endpoint.com ``` ### Cross-Region Replication [#cross-region-replication] For disaster recovery, enable cross-region replication on your S3 bucket. This automatically copies objects to a bucket in another region. *** ## Automated Backup Script [#automated-backup-script] Create a script to back up all Documenso components: ```bash #!/bin/bash # documenso-backup.sh set -e # Configuration BACKUP_DIR="/backup/documenso" RETENTION_DAYS=30 TIMESTAMP=$(date +%Y%m%d_%H%M%S) # Database connection DB_HOST="localhost" DB_USER="documenso" DB_NAME="documenso" # S3 configuration (if applicable) S3_BUCKET="your-documenso-bucket" # Create backup directory mkdir -p "$BACKUP_DIR/$TIMESTAMP" echo "Starting Documenso backup: $TIMESTAMP" # Database backup echo "Backing up database..." PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -F c \ -f "$BACKUP_DIR/$TIMESTAMP/database.dump" # Certificate backup (if file exists) if [ -f "/opt/documenso/cert.p12" ]; then echo "Backing up certificate..." cp /opt/documenso/cert.p12 "$BACKUP_DIR/$TIMESTAMP/cert.p12" fi # S3 backup (if configured) if [ -n "$S3_BUCKET" ]; then echo "Backing up S3 documents..." aws s3 sync "s3://$S3_BUCKET" "$BACKUP_DIR/$TIMESTAMP/documents/" --quiet fi # Compress backup echo "Compressing backup..." tar -czf "$BACKUP_DIR/documenso_$TIMESTAMP.tar.gz" -C "$BACKUP_DIR" "$TIMESTAMP" rm -rf "$BACKUP_DIR/$TIMESTAMP" # Calculate checksum sha256sum "$BACKUP_DIR/documenso_$TIMESTAMP.tar.gz" > "$BACKUP_DIR/documenso_$TIMESTAMP.sha256" # Remove old backups echo "Removing backups older than $RETENTION_DAYS days..." find "$BACKUP_DIR" -name "documenso_*.tar.gz" -mtime +$RETENTION_DAYS -delete find "$BACKUP_DIR" -name "documenso_*.sha256" -mtime +$RETENTION_DAYS -delete echo "Backup completed: documenso_$TIMESTAMP.tar.gz" ``` ```bash #!/bin/bash # documenso-backup-docker.sh set -e BACKUP_DIR="/backup/documenso" TIMESTAMP=$(date +%Y%m%d_%H%M%S) COMPOSE_PROJECT="documenso-production" mkdir -p "$BACKUP_DIR" echo "Starting backup: $TIMESTAMP" # Database backup via Docker docker compose -p "$COMPOSE_PROJECT" exec -T database \ pg_dump -U documenso -F c documenso > "$BACKUP_DIR/database_$TIMESTAMP.dump" # Certificate backup (copy from container if needed) docker compose -p "$COMPOSE_PROJECT" cp \ documenso:/opt/documenso/cert.p12 "$BACKUP_DIR/cert_$TIMESTAMP.p12" 2>/dev/null || true echo "Backup completed" ``` Make the script executable: ```bash chmod +x /usr/local/bin/documenso-backup.sh ``` *** ## Backup Retention [#backup-retention] Implement a retention policy to balance storage costs with recovery needs. ### Recommended Retention Schedule [#recommended-retention-schedule] | Backup Type | Retention | Use Case | | ----------- | --------- | ----------------------- | | Hourly | 24 hours | Recent changes recovery | | Daily | 7 days | Short-term recovery | | Weekly | 4 weeks | Medium-term recovery | | Monthly | 12 months | Long-term archival | ### Retention Script [#retention-script] Add retention logic to your backup script: ```bash # Remove hourly backups older than 24 hours find "$BACKUP_DIR/hourly" -name "*.dump" -mtime +1 -delete # Remove daily backups older than 7 days find "$BACKUP_DIR/daily" -name "*.dump" -mtime +7 -delete # Remove weekly backups older than 4 weeks find "$BACKUP_DIR/weekly" -name "*.dump" -mtime +28 -delete # Remove monthly backups older than 12 months find "$BACKUP_DIR/monthly" -name "*.dump" -mtime +365 -delete ``` ### Schedule Backups [#schedule-backups] Add to crontab (`crontab -e`): ``` # Daily backup at 2:00 AM 0 2 * * * /usr/local/bin/documenso-backup.sh >> /var/log/documenso-backup.log 2>&1 # Hourly backup (for high-activity instances) 0 * * * * /usr/local/bin/documenso-backup-hourly.sh >> /var/log/documenso-backup.log 2>&1 ``` Create `/etc/systemd/system/documenso-backup.service`: ```ini [Unit] Description=Documenso Backup [Service] Type=oneshot ExecStart=/usr/local/bin/documenso-backup.sh User=root ``` Create `/etc/systemd/system/documenso-backup.timer`: ```ini [Unit] Description=Daily Documenso Backup [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target ``` Enable the timer: ```bash systemctl daemon-reload systemctl enable --now documenso-backup.timer ``` *** ## Restore Procedures [#restore-procedures] ### Restore Database [#restore-database] | Flag | Description | | ---- | -------------------------------------- | | `-c` | Drop existing objects before restoring | | `-d` | Target database | Direct: ```bash pg_restore -h localhost -U documenso -d documenso -c documenso_backup.dump ``` Docker Compose: ```bash docker compose exec -T database pg_restore -U documenso -d documenso -c < documenso_backup.dump ``` ```bash psql -h localhost -U documenso -d documenso < documenso_backup.sql ``` For compressed backups: ```bash gunzip -c documenso_backup.sql.gz | psql -h localhost -U documenso -d documenso ``` ### Restore Certificate [#restore-certificate] Copy the certificate back to the expected location: ```bash cp /backup/documenso/cert.p12 /opt/documenso/cert.p12 chmod 644 /opt/documenso/cert.p12 chown 1001:1001 /opt/documenso/cert.p12 ``` ### Restore S3 Documents [#restore-s3-documents] Sync documents back to S3: ```bash aws s3 sync /backup/documenso/documents/ s3://your-documenso-bucket ``` ### Full Restore Procedure [#full-restore-procedure] {/* prettier-ignore */} ### Stop Documenso [#stop-documenso] ```bash docker compose stop documenso ``` ### Restore the database [#restore-the-database] ```bash docker compose exec -T database pg_restore -U documenso -d documenso -c < backup.dump ``` ### Restore the certificate [#restore-the-certificate] ```bash docker compose cp cert.p12 documenso:/opt/documenso/cert.p12 ``` ### Restore S3 documents (if applicable) [#restore-s3-documents-if-applicable] ```bash aws s3 sync /backup/documents/ s3://your-bucket ``` ### Start Documenso [#start-documenso] ```bash docker compose start documenso ``` ### Verify the restore [#verify-the-restore] ```bash curl http://localhost:3000/api/health ``` *** ## Testing Backups [#testing-backups] Untested backups are not backups. Regularly verify that you can restore from your backups. ### Monthly Restore Test [#monthly-restore-test] {/* prettier-ignore */} ### Create a test environment [#create-a-test-environment] Use a separate database and different port. ### Restore backup [#restore-backup] Restore your backup to the test environment. ### Verify [#verify] * Users can log in * Documents display correctly * Signing works with the restored certificate ### Document any issues [#document-any-issues] Record any problems found during the test. ### Tear down [#tear-down] Remove the test environment when done. ### Automated Restore Verification [#automated-restore-verification] Add verification to your backup script: ```bash #!/bin/bash # verify-backup.sh BACKUP_FILE="$1" TEST_DB="documenso_restore_test" # Create test database psql -h localhost -U postgres -c "CREATE DATABASE $TEST_DB" # Restore backup pg_restore -h localhost -U postgres -d "$TEST_DB" "$BACKUP_FILE" # Verify table counts USERS=$(psql -h localhost -U postgres -d "$TEST_DB" -t -c "SELECT COUNT(*) FROM \"User\"") DOCUMENTS=$(psql -h localhost -U postgres -d "$TEST_DB" -t -c "SELECT COUNT(*) FROM \"Document\"") echo "Verified backup contains: $USERS users, $DOCUMENTS documents" # Cleanup psql -h localhost -U postgres -c "DROP DATABASE $TEST_DB" ``` ### Backup Integrity Checks [#backup-integrity-checks] Verify backup file integrity: ```bash # Check checksum sha256sum -c documenso_backup.sha256 # Verify pg_dump format pg_restore --list documenso_backup.dump > /dev/null && echo "Backup is valid" ``` *** ## Managed Database Backups [#managed-database-backups] If using a managed PostgreSQL service, configure their backup features. * Enable automated backups (Settings > Backup) * Set retention period (7-35 days) * Enable Point-in-Time Recovery * Daily backups included (Pro plan) * Point-in-Time Recovery available * Access via Dashboard > Settings > Database * Automatic branching for instant snapshots * Create branches for backup purposes * Daily backups with 7-day retention * Enable in control panel Managed services handle backup automation, but you should still perform occasional manual backups to your own storage for additional protection. *** ## Off-Site Backup Storage [#off-site-backup-storage] Store backups in a separate location from your production system. ### Options [#options] | Storage Type | Pros | Cons | | --------------- | ----------------------------------- | ---------------------- | | AWS S3 | Durable, versioned, lifecycle rules | Cost for large backups | | Backblaze B2 | Low cost, S3-compatible | Egress fees | | rsync to remote | Simple, no vendor lock-in | Requires server | | Encrypted USB | Air-gapped, no network required | Manual process | ### Off-Site Upload [#off-site-upload] ```bash aws s3 cp documenso_backup.tar.gz s3://your-backup-bucket/documenso/ ``` Encrypt backups before storing off-site: ```bash # Encrypt gpg --symmetric --cipher-algo AES256 documenso_backup.tar.gz # Decrypt gpg --decrypt documenso_backup.tar.gz.gpg > documenso_backup.tar.gz ``` *** ## See Also [#see-also] * [Upgrades](/docs/self-hosting/maintenance/upgrades) - Backup before upgrading * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common backup issues * [Database Configuration](/docs/self-hosting/configuration/database) - Database setup reference * [Storage Configuration](/docs/self-hosting/configuration/storage) - S3 storage setup # Maintenance # Troubleshooting ## General Debugging [#general-debugging] ### Checking Application Logs [#checking-application-logs] Application logs are your primary source of debugging information. ```bash docker logs documenso ``` ```bash docker compose logs -f documenso ``` ```bash kubectl logs -l app.kubernetes.io/name=documenso -n documenso -f ``` ### Filtering Logs [#filtering-logs] Filter logs to find specific errors: ```bash # Docker - find errors docker logs documenso 2>&1 | grep -i error # Docker - find warnings docker logs documenso 2>&1 | grep -i warn # Show last 100 lines docker logs --tail 100 documenso ``` ### Health Checks [#health-checks] Documenso exposes a health endpoint to verify the application is running: ```bash curl http://localhost:3000/api/health ``` A healthy response returns HTTP 200. If the health check fails: {/* prettier-ignore */} Check application logs for startup errors Verify database connectivity Confirm environment variables are set correctly ### Container Status [#container-status] Check if containers are running and healthy: ```bash docker ps -a | grep documenso ``` ```bash docker compose ps ``` ```bash kubectl get pods -n documenso -o wide ``` ### Environment Variable Verification [#environment-variable-verification] Verify environment variables are set correctly inside the container: ```bash docker exec documenso printenv | grep NEXT ``` ```bash docker compose exec documenso printenv | grep NEXT ``` Be cautious when printing environment variables as they may contain sensitive information like passwords and API keys. *** ## Application Issues [#application-issues] ### Container Fails to Start [#container-fails-to-start] **Symptoms:** Container exits immediately or enters a crash loop. **Check the exit code:** ```bash docker inspect documenso --format='{{.State.ExitCode}}' ``` | Exit Code | Meaning | | --------- | -------------------------- | | 0 | Normal exit | | 1 | Application error | | 137 | Out of memory (OOM killed) | | 139 | Segmentation fault | | 143 | Terminated by SIGTERM | **Common causes and solutions:** | Cause | Solution | | ----------------------------- | ----------------------------------------------------- | | Missing required env variable | Check logs for "required environment variable" errors | | Database connection failure | Verify database URL and connectivity | | Port already in use | Change the port mapping or stop conflicting services | | Insufficient memory | Increase container memory limits | | Invalid certificate | Check certificate path and passphrase | ### Application Crashes After Startup [#application-crashes-after-startup] **Symptoms:** Application starts but crashes during operation. **Check for memory issues:** ```bash # Docker docker stats documenso # Kubernetes kubectl top pod -n documenso ``` **Solutions:** {/* prettier-ignore */} Increase memory limits in Docker or Kubernetes configuration Check for memory leaks in custom integrations Review application logs around the time of crash ### Startup Timeout in Kubernetes [#startup-timeout-in-kubernetes] **Symptoms:** Pod marked as unhealthy, enters CrashLoopBackOff. **Cause:** Startup probe fails before the application is ready. **Solution:** Increase startup probe timeout in your deployment: ```yaml startupProbe: httpGet: path: /api/health port: 3000 failureThreshold: 60 periodSeconds: 5 ``` This allows up to 5 minutes (60 x 5 seconds) for startup. ### Port Conflicts [#port-conflicts] **Symptoms:** "Port already in use" error. **Find what's using the port:** ```bash # Linux/macOS lsof -i :3000 # Or using netstat netstat -tlnp | grep 3000 ``` **Solutions:** {/* prettier-ignore */} Stop the conflicting service Change Documenso's port mapping: `-p 3001:3000` *** ## Database Issues [#database-issues] ### Connection Refused [#connection-refused] ``` Error: connect ECONNREFUSED 127.0.0.1:5432 ``` **Causes:** * PostgreSQL not running * Incorrect host in connection string * Firewall blocking connection * Database container not started yet **Solutions:** {/* prettier-ignore */} Verify PostgreSQL is running: ```bash # Direct PostgreSQL pg_isready -h localhost -p 5432 # Docker Compose docker compose ps database ``` Check connection string host matches database location For Docker Compose, ensure the database service is defined and started Verify network connectivity between containers ### Authentication Failed [#authentication-failed] ``` Error: password authentication failed for user "documenso" ``` **Causes:** * Incorrect password * Password contains unescaped special characters * User does not exist **Solutions:** {/* prettier-ignore */} URL-encode special characters in password (`@` becomes `%40`, `#` becomes `%23`) Verify credentials match database configuration Reset the password: ```sql ALTER USER documenso WITH PASSWORD 'newpassword'; ``` ### Database Does Not Exist [#database-does-not-exist] ``` Error: database "documenso" does not exist ``` **Solution:** Create the database: ```sql CREATE DATABASE documenso; GRANT ALL PRIVILEGES ON DATABASE documenso TO documenso; ``` ### Too Many Connections [#too-many-connections] ``` Error: too many connections for role "documenso" ``` **Causes:** * Connection pool exhausted * Multiple instances without connection pooling * Connections not being released **Solutions:** {/* prettier-ignore */} Reduce connection pool size in connection string: ``` postgresql://user:password@host:5432/documenso?connection_limit=5 ``` Increase PostgreSQL `max_connections` Implement connection pooling with PgBouncer ### Migration Failures [#migration-failures] **Symptoms:** Application fails to start with migration errors. **Check migration status:** ```sql SELECT * FROM _prisma_migrations WHERE finished_at IS NULL; ``` **Common causes:** | Cause | Solution | | ------------------------- | --------------------------------------------- | | Database locked | Wait for other processes to complete | | Insufficient disk | Free up disk space on database server | | Permission denied | Grant schema modification permissions to user | | Connection through pooler | Use direct database connection for migrations | **Retry failed migration:** {/* prettier-ignore */} Fix the underlying issue Mark migration as rolled back: ```sql UPDATE _prisma_migrations SET rolled_back_at = NOW() WHERE migration_name = 'failed_migration'; ``` Restart the application ### SSL Required [#ssl-required] ``` Error: SSL/TLS required ``` **Solution:** Add SSL mode to connection string: ``` postgresql://user:password@host:5432/documenso?sslmode=require ``` *** ## Email Issues [#email-issues] ### Emails Not Sending [#emails-not-sending] **Symptoms:** No emails received, no errors in logs. **Diagnostic steps:** {/* prettier-ignore */} Check email configuration: ```bash docker exec documenso printenv | grep SMTP ``` Verify all required variables are set: | Variable | Required | | -------------------------------- | -------- | | `NEXT_PRIVATE_SMTP_TRANSPORT` | Yes | | `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Yes | | `NEXT_PRIVATE_SMTP_FROM_NAME` | Yes | Test SMTP connectivity: ```bash # Test connection to SMTP server telnet smtp.example.com 587 ``` ### SMTP Connection Timeout [#smtp-connection-timeout] ``` Error: Connection timeout ``` **Causes:** * Incorrect SMTP hostname or port * Firewall blocking outbound SMTP * SMTP server unreachable **Solutions:** {/* prettier-ignore */} Verify hostname and port match provider documentation Check outbound firewall rules for ports 25, 465, 587 Test connectivity from the container: ```bash docker exec documenso nc -zv smtp.example.com 587 ``` ### SMTP Authentication Failed [#smtp-authentication-failed] ``` Error: Invalid login or authentication failed ``` **Causes:** * Incorrect username or password * Account requires app-specific password * Account security restrictions **Solutions:** {/* prettier-ignore */} Double-check credentials For Gmail/Microsoft with 2FA, generate an app password Check provider for security alerts or blocked sign-ins ### Emails Going to Spam [#emails-going-to-spam] **Causes:** * Missing SPF record * Missing DKIM signature * No DMARC policy * Poor sender reputation **Solutions:** {/* prettier-ignore */} Configure SPF record for your domain Set up DKIM signing with your email provider Add a DMARC policy Use a reputable email provider Test deliverability at [mail-tester.com](https://www.mail-tester.com) ### From Address Not Authorized [#from-address-not-authorized] ``` Error: Sender address rejected ``` **Cause:** The `NEXT_PRIVATE_SMTP_FROM_ADDRESS` is not authorized to send from your email provider. **Solutions:** {/* prettier-ignore */} Verify the domain in your email provider's settings Use an email address from a verified domain Check provider-specific sender authorization requirements *** ## Signing Issues [#signing-issues] ### No Certificate Found [#no-certificate-found] ``` Error: No certificate found for local signing ``` **Causes:** * Certificate path not configured * Certificate file not mounted * Environment variable not set **Solutions:** {/* prettier-ignore */} Verify certificate configuration: ```bash docker exec documenso printenv | grep SIGNING ``` Check file exists at configured path: ```bash docker exec documenso ls -la /opt/documenso/cert.p12 ``` Ensure either `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` or `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` is set ### Certificate Permission Denied [#certificate-permission-denied] ``` Error: EACCES: permission denied, open '/opt/documenso/cert.p12' ``` **Cause:** Application cannot read the certificate file. **Solution:** Fix file permissions: ```bash # On host before mounting sudo chown 1001 certificate.p12 chmod 400 certificate.p12 ``` ### Invalid Certificate Password [#invalid-certificate-password] ``` Error: mac verify failure ``` **Cause:** Incorrect passphrase for the certificate. **Solutions:** {/* prettier-ignore */} Verify `NEXT_PRIVATE_SIGNING_PASSPHRASE` matches the certificate password Check for trailing whitespace or encoding issues Test the password: ```bash openssl pkcs12 -in certificate.p12 -noout ``` ### Failed to Get Private Key [#failed-to-get-private-key] ``` Error: Failed to get private key bags ``` **Cause:** Certificate has no password or incompatible encryption. **Solution:** Re-create the certificate with a password and compatible encryption: ```bash openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \ -keypbe PBE-SHA1-3DES \ -certpbe PBE-SHA1-3DES \ -macalg sha1 ``` You must set a non-empty password when creating the certificate. Certificates without passwords will fail. ### Certificate Expired [#certificate-expired] ``` Error: Certificate has expired ``` **Solution:** {/* prettier-ignore */} Check expiration date: ```bash openssl pkcs12 -in certificate.p12 -nokeys | openssl x509 -noout -dates ``` Generate a new certificate or obtain a renewed one from your CA Update the certificate file and restart Documenso ### Base64 Decode Error [#base64-decode-error] ``` Error: Invalid base64 in NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS ``` **Cause:** Incorrect base64 encoding or line breaks in the value. **Solution:** Encode without line breaks: ```bash # macOS base64 -i certificate.p12 | tr -d '\n' # Linux base64 -w 0 certificate.p12 ``` *** ## Storage Issues [#storage-issues] ### S3 Access Denied [#s3-access-denied] ``` Error: Access Denied ``` **Causes:** * Incorrect IAM credentials * Bucket policy doesn't allow operations * CORS not configured **Solutions:** {/* prettier-ignore */} Verify IAM user has required permissions: ```json { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::your-bucket/*" } ``` Configure CORS for presigned URL uploads Verify credentials are correct and not expired ### S3 Bucket Not Found [#s3-bucket-not-found] ``` Error: The specified bucket does not exist ``` **Causes:** * Bucket name typo * Bucket in different region * Bucket doesn't exist **Solutions:** {/* prettier-ignore */} Verify bucket exists: ```bash aws s3 ls s3://your-bucket ``` Check `NEXT_PRIVATE_UPLOAD_REGION` matches bucket region Create the bucket if it doesn't exist ### Signature Mismatch [#signature-mismatch] ``` Error: SignatureDoesNotMatch ``` **Causes:** * Incorrect secret access key * Clock skew between server and S3 **Solutions:** {/* prettier-ignore */} Verify credentials are correct Synchronize server time using NTP: ```bash # Check time date # Sync time (Linux) sudo ntpdate pool.ntp.org ``` ### Path-Style Access Required [#path-style-access-required] ``` Error: The bucket you are attempting to access must be addressed using the specified endpoint ``` **Cause:** S3-compatible service requires path-style URLs. **Solution:** Enable path-style access: ```bash NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true ``` ### File Upload Timeout [#file-upload-timeout] **Symptoms:** Large file uploads fail or timeout. **Causes:** * Upload size exceeds limit * Network timeout * Reverse proxy timeout **Solutions:** {/* prettier-ignore */} Check configured upload limit: `NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT` Increase reverse proxy timeout (nginx `client_body_timeout`) For very large files, verify S3 multipart upload is working *** ## Performance Issues [#performance-issues] ### Slow Response Times [#slow-response-times] **Diagnostic steps:** {/* prettier-ignore */} Check container resource usage: ```bash docker stats documenso ``` Monitor database performance: ```sql SELECT query, calls, mean_time FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 10; ``` Check for memory pressure or CPU throttling **Common causes and solutions:** | Cause | Solution | | ------------------- | --------------------------------------------- | | Insufficient memory | Increase container memory limits | | Database slow | Add indexes, increase database resources | | Network latency | Move database closer to application | | Large documents | Enable S3 storage instead of database storage | ### High Memory Usage [#high-memory-usage] **Symptoms:** Container OOM killed, slow response times. **Solutions:** {/* prettier-ignore */} Increase memory limits: ```yaml # Docker Compose services: documenso: deploy: resources: limits: memory: 2G ``` If using database storage, switch to S3 for large files Review and optimize any custom integrations ### Database Performance Degradation [#database-performance-degradation] **Symptoms:** Slow queries, timeouts. **Diagnostic queries:** ```sql -- Check table sizes SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC; -- Find long-running queries SELECT pid, now() - pg_stat_activity.query_start AS duration, query FROM pg_stat_activity WHERE state != 'idle' ORDER BY duration DESC; ``` **Solutions:** {/* prettier-ignore */} Run VACUUM ANALYZE to update statistics: ```sql VACUUM ANALYZE; ``` Check for missing indexes on frequently queried columns Consider upgrading database resources Archive old documents and audit logs ### Slow Document Signing [#slow-document-signing] **Symptoms:** Document completion takes a long time. **Causes:** * Timestamp server latency * Large PDF files * Certificate issues **Solutions:** {/* prettier-ignore */} Use a faster timestamp server or one geographically closer Optimize PDF file sizes before upload Verify certificate is valid and not causing retries *** ## Network Issues [#network-issues] ### Cannot Reach Application [#cannot-reach-application] **Diagnostic steps:** {/* prettier-ignore */} Verify container is running and port is exposed: ```bash docker ps | grep documenso ``` Check port binding: ```bash netstat -tlnp | grep 3000 ``` Test local connectivity: ```bash curl http://localhost:3000/api/health ``` ### Reverse Proxy Issues [#reverse-proxy-issues] **Symptoms:** 502 Bad Gateway, connection refused through proxy. **Solutions:** {/* prettier-ignore */} Verify upstream is reachable from the proxy Check proxy configuration points to correct port Ensure headers are forwarded correctly: ```nginx proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; ``` ### WebSocket Connection Failures [#websocket-connection-failures] **Symptoms:** Real-time updates not working. **Solution:** Configure reverse proxy for WebSocket support: ```nginx location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } ``` ### DNS Resolution Failures [#dns-resolution-failures] ``` Error: getaddrinfo ENOTFOUND hostname ``` **Causes:** * Incorrect hostname * DNS not configured in container * Network isolation **Solutions:** {/* prettier-ignore */} Verify hostname is correct Check DNS configuration: ```bash docker exec documenso cat /etc/resolv.conf ``` For Docker Compose, use service names for inter-container communication *** ## Getting Help [#getting-help] ### Before Asking for Help [#before-asking-for-help] Gather this information before seeking support: {/* prettier-ignore */} **Documenso version:** ```bash docker inspect documenso --format='{{.Config.Image}}' ``` **Deployment method:** Docker, Docker Compose, Kubernetes, or other **Relevant logs:** Filter to show errors around the issue **Steps to reproduce:** What actions trigger the problem **Environment:** Cloud provider, OS, database type ### Community Support [#community-support] * **[GitHub Discussions](https://github.com/documenso/documenso/discussions)** * **[Discord](https://documen.so/discord)**: Join the Documenso community Discord * **[GitHub Issues](https://github.com/documenso/documenso/issues)**: For confirmed bugs, open an issue ### Reporting Bugs [#reporting-bugs] When reporting bugs, include: {/* prettier-ignore */} Clear description of the expected vs actual behavior Steps to reproduce Documenso version Relevant logs (redact sensitive information) Environment details ### Security Issues [#security-issues] For security vulnerabilities, do not open a public issue. Contact [security@documenso.com](mailto:security@documenso.com) directly. *** ## Troubleshooting Checklist [#troubleshooting-checklist] Use this checklist when diagnosing issues: Run `docker logs documenso` or `docker compose logs documenso` (or `kubectl logs` for Kubernetes) and look for error messages. Compare your `.env` or configuration against [Environment Variables](/docs/self-hosting/configuration/environment). Test the connection string with `psql` or the database CLI. Ensure the database is running and reachable. Run `curl http://localhost:3000/api/health` and verify it returns a successful response. Use `docker stats` or your container runtime's monitoring to ensure adequate resources. Check `curl http://localhost:3000/api/certificate-status` and ensure file permissions are correct. Verify SMTP settings in [Email Configuration](/docs/self-hosting/configuration/email) and check logs for delivery errors. Ensure outbound connections to SMTP, S3, and other external services are allowed. Check release notes and compare your configuration before and after the change. *** ## See Also [#see-also] * [Backups](/docs/self-hosting/maintenance/backups) - Backup and restore procedures * [Upgrades](/docs/self-hosting/maintenance/upgrades) - Upgrade procedures * [Database Configuration](/docs/self-hosting/configuration/database) - Database setup reference * [Email Configuration](/docs/self-hosting/configuration/email) - Email troubleshooting * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Certificate troubleshooting * [Storage Configuration](/docs/self-hosting/configuration/storage) - Storage troubleshooting # Upgrades ## Version Policy [#version-policy] Documenso follows [Semantic Versioning](https://semver.org/): | Version Type | Format | Description | | ------------ | ------- | ------------------------------------------ | | Major | `x.0.0` | Breaking changes, may require manual steps | | Minor | `x.y.0` | New features, backward compatible | | Patch | `x.y.z` | Bug fixes, security patches | Review the [release notes](https://github.com/documenso/documenso/releases) before upgrading to understand what changes are included. ### Image Tags [#image-tags] | Tag | Description | Use Case | | --------- | -------------------------------- | -------------------- | | `latest` | Most recent stable release | Development, testing | | `x.y.z` | Specific version (e.g., `1.5.0`) | Production | | `release` | Latest build from release branch | Pre-release testing | For production deployments, pin to a specific version tag to avoid unexpected updates. ## Checking for Updates [#checking-for-updates] ### View Current Version [#view-current-version] Check your running Documenso version: ```bash docker inspect documenso --format='{{.Config.Image}}' ``` ```bash docker compose images ``` ```bash kubectl get deployment documenso -n documenso -o jsonpath='{.spec.template.spec.containers[0].image}' ``` ### Check for New Releases [#check-for-new-releases] View available releases on GitHub: ```bash curl -s https://api.github.com/repos/documenso/documenso/releases/latest | grep tag_name ``` Or visit the [releases page](https://github.com/documenso/documenso/releases). ### Check Available Docker Tags [#check-available-docker-tags] List available image tags: ```bash curl -s https://hub.docker.com/v2/repositories/documenso/documenso/tags | jq -r '.results[].name' ``` ```bash curl -s https://api.github.com/orgs/documenso/packages/container/documenso/versions | jq -r '.[].metadata.container.tags[]' ``` ## Backup Before Upgrading [#backup-before-upgrading] Always back up your database before upgrading. Upgrades may include database migrations that cannot be reversed. ### Database Backup [#database-backup] ```bash docker compose exec database pg_dump -U documenso documenso > backup-$(date +%Y%m%d-%H%M%S).sql ``` ```bash kubectl exec -n documenso deploy/postgres -- pg_dump -U documenso documenso > backup-$(date +%Y%m%d-%H%M%S).sql ``` Use your database provider's backup tools or `pg_dump`: ```bash pg_dump "postgresql://user:password@host:5432/documenso" > backup-$(date +%Y%m%d-%H%M%S).sql ``` ### Configuration Backup [#configuration-backup] Back up your environment configuration: ```bash cp .env .env.backup-$(date +%Y%m%d) ``` Export your secrets and configmaps: ```bash kubectl get secret documenso-secrets -n documenso -o yaml > secrets-backup.yaml kubectl get configmap documenso-config -n documenso -o yaml > configmap-backup.yaml ``` See [Backups](/docs/self-hosting/maintenance/backups) for automated backup strategies. ## Upgrade Process [#upgrade-process] ### Docker [#docker] {/* prettier-ignore */} ### Pull the new image [#pull-the-new-image] ```bash docker pull documenso/documenso: ``` Replace `` with your target version. ### Stop the current container [#stop-the-current-container] ```bash docker stop documenso docker rm documenso ``` ### Start with the new image [#start-with-the-new-image] ```bash docker run -d \ --name documenso \ -p 3000:3000 \ --env-file .env \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ documenso/documenso: ``` ### Verify the upgrade [#verify-the-upgrade] ```bash docker logs -f documenso ``` Wait for "Ready" or "Listening on port 3000" in the output. Test the health endpoint: ```bash curl http://localhost:3000/api/health ``` ### Docker Compose [#docker-compose] {/* prettier-ignore */} ### Update the image tag [#update-the-image-tag] Edit `compose.yml` or your `.env` file to specify the new version: ```yaml services: documenso: image: documenso/documenso: ``` Or if using environment variable substitution: ```bash # In .env DOCUMENSO_VERSION= ``` ```yaml # In compose.yml services: documenso: image: documenso/documenso:${DOCUMENSO_VERSION:-latest} ``` ### Pull the new image [#pull-the-new-image-1] ```bash docker compose pull ``` ### Apply the update [#apply-the-update] ```bash docker compose --env-file .env up -d ``` Docker Compose recreates containers that have changed images. ### Verify the upgrade [#verify-the-upgrade-1] ```bash docker compose ps docker compose logs -f documenso ``` Confirm the container is running and healthy. ### Kubernetes [#kubernetes] {/* prettier-ignore */} ### Update the deployment image [#update-the-deployment-image] Edit the deployment directly: ```bash kubectl set image deployment/documenso \ documenso=documenso/documenso: \ -n documenso ``` Or update your manifest file: ```yaml spec: template: spec: containers: - name: documenso image: documenso/documenso: ``` Then apply: ```bash kubectl apply -f deployment.yaml ``` ### Monitor the rollout [#monitor-the-rollout] ```bash kubectl rollout status deployment/documenso -n documenso ``` Watch pods transition: ```bash kubectl get pods -n documenso -w ``` ### Verify the upgrade [#verify-the-upgrade-2] Check the new pods are running: ```bash kubectl get pods -n documenso -o wide ``` Test the health endpoint: ```bash kubectl port-forward svc/documenso 3000:80 -n documenso & curl http://localhost:3000/api/health ``` ## Database Migrations [#database-migrations] Documenso runs database migrations automatically when the container starts. No manual intervention is required. ### Migration Process [#migration-process] {/* prettier-ignore */} ### Container starts [#container-starts] The container starts and connects to the database. ### Prisma checks for migrations [#prisma-checks-for-migrations] Prisma checks for pending migrations. ### Migrations run [#migrations-run] Migrations are applied in order. ### Application starts [#application-starts] The application starts after migrations complete. ### Checking Migration Status [#checking-migration-status] View migration logs: ```bash docker logs documenso 2>&1 | grep -i migration ``` ```bash kubectl logs -l app.kubernetes.io/name=documenso -n documenso | grep -i migration ``` ### Slow Migrations [#slow-migrations] Large databases may take longer to migrate. The startup probe in Kubernetes allows up to 150 seconds (30 failures x 5 second period) for migrations to complete. If migrations take longer: {/* prettier-ignore */} ### Adjust startup probe [#adjust-startup-probe] Increase `failureThreshold` on the startup probe. ### Run migrations separately (optional) [#run-migrations-separately-optional] Consider running migrations manually before upgrading (see Manual Migration below). ### Manual Migration (Advanced) [#manual-migration-advanced] To run migrations manually before upgrading: ```bash # Pull the new image docker pull documenso/documenso: # Run migrations only docker run --rm \ -e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso" \ documenso/documenso: \ npx prisma migrate deploy ``` This is an advanced operation. In most cases, automatic migrations are sufficient. ## Breaking Changes [#breaking-changes] Major version upgrades may include breaking changes that require manual steps. ### Before a Major Upgrade [#before-a-major-upgrade] {/* prettier-ignore */} ### Read release notes [#read-release-notes] Read the [release notes](https://github.com/documenso/documenso/releases) carefully. ### Check environment variables [#check-environment-variables] Check for required environment variable changes. ### Review configuration [#review-configuration] Review any configuration deprecations. ### Back up database [#back-up-database] Back up your database. ### Plan for downtime [#plan-for-downtime] Plan for potential downtime. ### Common Breaking Changes [#common-breaking-changes] | Change Type | Action Required | | ------------------------- | -------------------------------------------- | | New required env variable | Add the variable to your configuration | | Removed env variable | Remove from configuration to avoid confusion | | Renamed env variable | Update to the new name | | Database schema change | Automatic migration (back up first) | | API changes | Update integrations using the API | ### Environment Variable Changes [#environment-variable-changes] Compare your current environment with the latest example: ```bash # Download latest example curl -O https://raw.githubusercontent.com/documenso/documenso/main/.env.example # Compare with your current config diff .env .env.example ``` ## Rollback Procedure [#rollback-procedure] If an upgrade fails, roll back to the previous version. ### Container Rollback [#container-rollback] ```bash # Stop the new container docker stop documenso docker rm documenso # Start with the previous image docker run -d \ --name documenso \ -p 3000:3000 \ --env-file .env \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ documenso/documenso: ``` ```bash # Revert the image tag in compose.yml or .env # Then recreate containers docker compose --env-file .env up -d ``` ```bash # View rollout history kubectl rollout history deployment/documenso -n documenso # Rollback to previous version kubectl rollout undo deployment/documenso -n documenso # Or rollback to a specific revision kubectl rollout undo deployment/documenso -n documenso --to-revision=2 ``` ### Database Rollback [#database-rollback] Database migrations cannot be automatically reversed. If you need to rollback after migrations have run, restore from your backup. Restore from backup: ```bash docker compose exec -T database psql -U documenso documenso < backup-20240115-120000.sql ``` ```bash psql "postgresql://user:password@host:5432/documenso" < backup-20240115-120000.sql ``` ## Troubleshooting Upgrades [#troubleshooting-upgrades] **Check logs:** `docker logs documenso` **Common causes:** * Missing env variable: check release notes for new required variables * Database connection error: verify database is accessible and credentials are valid * Migration failure: check logs for the specific migration error * Certificate error: verify certificate mount and passphrase **What to do:** * Check the specific error in the logs * Verify database connectivity * Ensure the database user has `ALTER` permissions * Check for sufficient disk space on the database server **Test connectivity:** ```bash docker run --rm postgres:15 psql "postgresql://user:password@host:5432/documenso" -c "SELECT 1" ``` **If the application starts but behaves unexpectedly:** * Clear browser cache and cookies * Check for new environment variables in release notes * Verify all environment variables are correctly set * Check application logs: ```bash docker logs documenso 2>&1 | grep -i -E "(warn|error)" ``` **If performance degrades:** * New indexes being created can cause temporary slowdown (normal) * Monitor database CPU and memory usage * Review release notes for known performance impacts * Allow time for database query plans to stabilize **If rollback also fails:** * Verify the old image tag exists and is accessible * Check that your backup is valid and complete * Review environment configuration for any changes * Consider restoring to a clean database from backup ## Upgrade Checklist [#upgrade-checklist] Use this checklist for each upgrade: * [ ] Review release notes for the target version * [ ] Check for breaking changes or new requirements * [ ] Back up the database * [ ] Back up environment configuration * [ ] Note the current image tag for potential rollback * [ ] Pull the new image * [ ] Apply the upgrade * [ ] Verify container starts successfully * [ ] Check logs for migration completion * [ ] Test the health endpoint * [ ] Verify core functionality (login, document upload, signing) * [ ] Monitor for errors in logs *** ## See Also [#see-also] * [Backups](/docs/self-hosting/maintenance/backups) - Automated backup strategies * [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common issues and solutions * [Environment Variables](/docs/self-hosting/configuration/environment) - Configuration reference # Add Fields ## Overview [#overview] Fields are placeholders that recipients fill in when signing your document. You drag fields from the palette onto your document, position them where you want information collected, and assign them to specific recipients. Document fields panel showing available field types ## Add a Field to Your Document [#add-a-field-to-your-document] {/* prettier-ignore */} ### Select a recipient [#select-a-recipient] In the document editor, select the recipient you want to assign fields to from the recipient list in the sidebar. The selected recipient's color highlights which fields belong to them. ### Drag a field onto the document [#drag-a-field-onto-the-document] Click and drag a field type from the field palette onto your document. Available field types include: Captures the recipient's legally binding signature Abbreviated signature for acknowledging pages or clauses Recipient's full name (auto-filled when available) Recipient's email address (auto-filled) Date the field was completed (auto-filled) Free-form text input Numeric input with optional validation Multiple selections from options Single selection from options Single selection from a menu ### Position the field [#position-the-field] Drop the field where you want it on the document. You can drag it to reposition after placing. Each signer must have at least one Signature field assigned to them. The document cannot be sent without this. ## Assign Fields to Recipients [#assign-fields-to-recipients] Fields are color-coded by recipient. To change which recipient a field belongs to: {/* prettier-ignore */} ### Select the field [#select-the-field] Click on the field to select it. ### Change the recipient [#change-the-recipient] In the field properties panel, change the **Recipient** dropdown. ### Confirm the assignment [#confirm-the-assignment] The field color updates to match the new recipient. When you have multiple recipients, make sure each one has the appropriate fields assigned: * **Signers** need at least one Signature field * **Approvers** can have fields but don't require them * **Viewers** cannot have fields assigned * **Assistants** can pre-fill fields for other recipients ## Position and Resize Fields [#position-and-resize-fields] ### Moving Fields [#moving-fields] Click and drag any field to reposition it on the document. Fields snap to help with alignment. ### Resizing Fields [#resizing-fields] Click a field to select it, then drag the corner handles to resize. This is useful for: * Making signature fields larger for handwritten signatures * Expanding text fields to accommodate longer input * Fitting fields into existing form areas on your PDF ### Multi-Page Documents [#multi-page-documents] Navigate between pages using the page controls. Fields placed on a page stay on that page. You can place fields on any page of your document. ## Configure Field Properties [#configure-field-properties] Click on any field to open its properties panel. Available options vary by field type. ### Common Properties [#common-properties] | Property | Description | | --------- | ------------------------------------------------------------- | | Required | Recipient must complete this field to finish signing | | Read-only | Lock the field with a pre-filled value that cannot be changed | | Label | Text displayed above the field | | Font size | Size of text in the field (8-96px) | ### Text Field Properties [#text-field-properties] | Property | Description | | --------------- | ------------------------------------------ | | Placeholder | Hint text shown when the field is empty | | Default value | Pre-filled text that recipients can modify | | Character limit | Maximum characters allowed | | Text alignment | Left, center, or right | ### Number Field Properties [#number-field-properties] | Property | Description | | ------------- | ----------------------------- | | Minimum value | Lowest allowed number | | Maximum value | Highest allowed number | | Number format | Display format for the number | ### Selection Field Properties (Checkbox, Radio, Dropdown) [#selection-field-properties-checkbox-radio-dropdown] | Property | Description | | ----------------- | --------------------------------------------------- | | Options | List of selectable values | | Default selection | Pre-selected option(s) | | Direction | Vertical or horizontal layout (Checkbox/Radio only) | Dropdown field in the document editor For detailed information on all field types, see [Field Types](/docs/concepts/field-types). ## Common Field Patterns [#common-field-patterns] ### Signature Block [#signature-block] A typical signature block includes the following fields: 1. **Signature**: Where the recipient signs 2. **Name**: Prints their full name below 3. **Date**: Records when they signed Signature field in the document editor Place these fields together at the bottom of your document or wherever signature lines appear. ### Terms Acceptance [#terms-acceptance] For documents requiring explicit agreement to terms: {/* prettier-ignore */} ### Add a Checkbox field [#add-a-checkbox-field] Add a **Checkbox** field with options like "I agree to the terms and conditions". ### Mark as required [#mark-as-required] Mark the field as **Required**. ### Place near the terms [#place-near-the-terms] Place it near the terms text. ### Information Collection [#information-collection] When gathering additional information from signers: {/* prettier-ignore */} ### Add Text fields [#add-text-fields] Add **Text** fields for addresses, job titles, or company names. ### Add Number fields [#add-number-fields] Use **Number** fields for quantities, IDs, or phone numbers. ### Add Dropdown fields [#add-dropdown-fields] Use **Dropdown** fields for selections like country or department. ### Set placeholders [#set-placeholders] Set **Placeholder** text to guide what information you need. ### Multiple Signers [#multiple-signers] When you have multiple people signing the same document: {/* prettier-ignore */} ### Create signature blocks per signer [#create-signature-blocks-per-signer] Create separate signature blocks for each signer. ### Assign fields to recipients [#assign-fields-to-recipients-1] Assign each block's fields to the correct recipient. ### Verify with color coding [#verify-with-color-coding] Use the recipient color coding to verify assignments. ### Optional: enable signing order [#optional-enable-signing-order] Consider enabling signing order if signers should sign in sequence. ## Edit or Delete Fields [#edit-or-delete-fields] Click on any field to select it and open its properties panel. Make your changes and they save automatically. Select the field and press **Delete** or **Backspace** on your keyboard. Alternatively, click the delete icon in the field properties panel. Select a field and copy it (**Ctrl+C** or **Cmd+C**), then paste (**Ctrl+V** or **Cmd+V**) to create a duplicate. This is useful when you need multiple similar fields. *** ## See Also [#see-also] * [Send Documents](/docs/users/documents/send) - Send for signing and start collecting signatures * [Create Templates](/docs/users/templates/create) - Save this field layout as a reusable template * [Field Types](/docs/concepts/field-types) - Detailed configuration options for all field types # Add Recipients ## Add a Recipient [#add-a-recipient] {/* prettier-ignore */} ### Open the document editor [#open-the-document-editor] You can access the document editor by clicking on a document in your Documents dashboard. If you're uploading a document, you'll be redirected to the document editor after the upload is complete automatically. ### Click add signer [#click-add-signer] In the Recipients section, click **+ Add Signer** to add a new recipient row. Recipients panel in the document editor ### Enter recipient details [#enter-recipient-details] Fill in the following information: * **Email**: The recipient's email address (required) * **Name**: The recipient's name (optional, but recommended for clarity) * **Role**: What the recipient needs to do (see [Recipient Roles](/docs/concepts/recipient-roles)) ## Recipient Information [#recipient-information] ### Email Address [#email-address] The email address is where the recipient receives their signing invitation. Make sure it's correct - once you send the document, the invitation goes to this address and cannot be changed. ### Name [#name] Adding a name helps recipients identify themselves when they receive the document. The name appears in: * The signing invitation email * The document activity log * The completed document's signature certificate If you leave the name blank, the email address is displayed instead. ## Assigning Roles [#assigning-roles] Each recipient needs a role that determines what actions they can take: | Role | What they do | | --------- | ----------------------------------------------- | | Signer | Must sign the document | | Approver | Must approve the document (signing is optional) | | Viewer | Must confirm they viewed the document | | Assistant | Pre-fills fields for other recipients | | CC | Receives a copy after the document is completed | For detailed information about each role, see [Recipient Roles](/docs/concepts/recipient-roles). The Assistant role is only available when sequential signing is enabled. ## Multiple Recipients [#multiple-recipients] You can add as many recipients as you need. Click **+ Add Signer** for each additional person. Common scenarios: Add yourself and the other party as signers. Add the main signer plus a viewer as witness. Add an approver first, then signers. Add CC recipients who receive the final document. ## Signing Order [#signing-order] By default, all recipients receive the document at the same time and can complete their actions in any order (parallel signing). You can change this to require recipients to act in a specific sequence. ### Parallel Signing (Default) [#parallel-signing-default] All recipients receive the document simultaneously. The document is completed when everyone finishes their required actions. Use this when: * The order doesn't matter * You want the fastest completion time * Recipients are independent of each other ### Sequential Signing [#sequential-signing] Recipients receive the document one at a time, in the order you specify. Each person must complete their action before the next person is notified. To enable sequential signing: {/* prettier-ignore */} ### Enable signing order [#enable-signing-order] Toggle on **Enable signing order** in the Recipients section. ### Set the order for each recipient [#set-the-order-for-each-recipient] Assign an order number to each recipient (1, 2, 3, etc.). ### Use the same number for parallel steps [#use-the-same-number-for-parallel-steps] Recipients with the same order number act simultaneously at that step. Sequential signing is required to use the Assistant role. Assistants must act before the signers whose fields they pre-fill. **Example workflow:** | Order | Recipient | Role | | ----- | ---------------- | --------- | | 1 | Admin Assistant | Assistant | | 2 | Department Head | Approver | | 3 | Contract Party A | Signer | | 3 | Contract Party B | Signer | | - | Legal Team | CC | In this example: 1. The assistant pre-fills information 2. The department head reviews and approves 3. Both contract parties sign at the same time 4. Legal receives a copy after completion ## Editing Recipients [#editing-recipients] You can modify recipient details anytime before sending the document: * **Change email or name**: Click on the field and type the new value * **Change role**: Select a different role from the dropdown * **Change signing order**: Adjust the order number (if sequential signing is enabled) After sending a document, you cannot change recipients. If you need different recipients, you'll need to create a new document. ## Removing Recipients [#removing-recipients] To remove a recipient, click the delete icon (trash) next to their row. This also removes any fields assigned to that recipient. You cannot remove recipients after the document has been sent. *** ## See Also [#see-also] * [Add Fields](/docs/users/documents/add-fields) - Place signature, text, date, and other fields on your document * [Send Documents](/docs/users/documents/send) - Send for signing and monitor progress * [Recipient Roles](/docs/concepts/recipient-roles) - Detailed information about each role # Direct Links ## Overview [#overview] Use direct links when you: * Need to collect signatures from people whose emails you don't have * Want to embed signing into your website or application * Are collecting signatures from a public audience (forms, petitions, agreements) ## Limitations [#limitations] * Direct links only work with templates, not individual documents * You cannot specify recipient details in advance - they enter their own information * Each direct link is tied to a single recipient role in the template * The template owner receives a notification for each completed signature *** ## Create a Direct Link [#create-a-direct-link] Direct links are created from templates. You'll need a template before you can generate a direct link. {/* prettier-ignore */} ### Go to your template [#go-to-your-template] Navigate to **Templates** in the sidebar and find the template you want to share. ### Enable direct link [#enable-direct-link] Click the three-dot menu on the template row and select **Direct link**. In the modal that appears, click **Enable direct link** to proceed. Enable document direct link modal ### Select the recipient [#select-the-recipient] Choose which recipient in your template will use the direct link: * **Select an existing recipient**: Pick from recipients already defined in your template * **Create one automatically**: Documenso creates a new "Direct Link Recipient" for you Choose direct link recipient The person who opens the direct link will fill in this recipient's fields. The direct link recipient cannot be edited or deleted while the direct link is active. Disable the direct link first if you need to modify this recipient. ### Copy the link [#copy-the-link] After selecting a recipient, you'll see your direct link: ``` https://app.documenso.com/d/ ``` Document direct link Copy this link and share it however you need - email, website, QR code, or social media. *** ## What Recipients See [#what-recipients-see] When someone opens your direct link: 1. They see the document with all fields assigned to the direct link recipient 2. They enter their name and email address 3. They complete any required fields (signature, text, checkboxes, etc.) 4. After signing, they receive a copy of the completed document via email If the recipient has a Documenso account with the email they entered, the document also appears in their Documents dashboard. *** ## Manage Direct Links [#manage-direct-links] ### Disable a direct link [#disable-a-direct-link] To temporarily stop accepting signatures: {/* prettier-ignore */} Go to **Templates** and find your template. Click the three-dot menu and select **Direct link**. Toggle the direct link off. The link URL remains the same. You can re-enable it later without generating a new link. ### Delete a direct link [#delete-a-direct-link] To permanently remove a direct link: {/* prettier-ignore */} Disable the direct link first (see above). Edit or delete the direct link recipient as needed. If you enable direct links again later, you'll get a new URL. *** ## Add Direct Links to Your Profile [#add-direct-links-to-your-profile] You can showcase templates with direct links on your public Documenso profile: {/* prettier-ignore */} Go to **Settings** > **Public Profile**. Enable your public profile if you haven't already. Add templates with active direct links to your profile. Visitors to your profile page can then sign those documents directly. Documenso profile with direct links This is useful for freelancers, consultants, or businesses who want clients to easily access common agreements. *** ## Use Cases [#use-cases] ### Public forms and agreements [#public-forms-and-agreements] Create a template for documents that anyone can sign: * Liability waivers * NDAs for event attendees * Consent forms * Petition signatures Share the direct link on your website, in emails, or via QR codes at physical locations. ### Embedded signing [#embedded-signing] Add signing capabilities to your website or app by embedding the direct link: ```html Sign our agreement ``` Or redirect users to the signing page after they complete another action in your application. ### Self-service contracts [#self-service-contracts] Let customers or partners initiate contracts themselves: {/* prettier-ignore */} Create a template with your standard terms. Share the direct link with customers or partners. Recipients sign and receive their copy automatically. You get notified and the signed document appears in your dashboard. *** ## Direct Links vs Standard Document Flow [#direct-links-vs-standard-document-flow] | Feature | Standard Flow | Direct Links | | ---------------------------- | ---------------------- | ---------------------------------------- | | Know recipient email upfront | Required | Not required | | Send email invitation | Yes | No (you share the link) | | Multiple recipients | All specified | One recipient via link, others specified | | Recipient enters their info | No | Yes | | Works with | Documents or Templates | Templates only | *** ## See Also [#see-also] * [Create a Template](/docs/users/templates/create) - Set up a template to use with direct links * [API: Templates](/docs/developers/api/templates) - Generate documents from templates programmatically # Documents ## Document Workflow [#document-workflow] 1. **[Upload](/docs/users/documents/upload)** - Upload your PDF and configure settings 2. **[Add Recipients](/docs/users/documents/add-recipients)** - Specify who needs to sign, approve, or view 3. **[Add Fields](/docs/users/documents/add-fields)** - Place signature, text, and other fields 4. **[Send](/docs/users/documents/send)** - Send for signing and monitor progress *** ## Additional Options [#additional-options] *** ## See Also [#see-also] * [Document Lifecycle](/docs/concepts/document-lifecycle) - Understanding document statuses * [Templates](/docs/users/templates) - Create reusable document templates # Send Documents ## Prerequisites [#prerequisites] Before sending, ensure your document has: * At least one recipient added * Signature fields assigned to each signer * Required email addresses for recipients with authentication enabled ## Send a Document [#send-a-document] {/* prettier-ignore */} Click **Send Document** in the document editor. Choose the distribution method: **Email** (recipients get signing links by email) or **None** (you get links to share manually). If using email, configure subject, message, and reply-to as needed. Click **Send** or **Generate Links** to send the document. Once sent, the document status changes from **Draft** to **Pending**. ## Email Customization [#email-customization] When sending via email, you can customize the email that recipients receive. There are 2 ways to customize the email. You can customize it before sending the document, after pressing the **Send** button. Document email settings Or you can customize it before sending the document, by clicking on the **Document Settings** button in the editor sidebar. On this page, you can also customize what emails are sent to the recipients. Editor email settings ### Reply-to email (optional) [#reply-to-email-optional] Set a reply-to address so recipient replies go to a specific email instead of the default. ### Subject (optional) [#subject-optional] Customize the email subject line. If left blank, recipients see a default subject based on their role: "Please sign this document" "Please approve this document" "Please view this document" For team documents, the subject includes the team name. ### Message (optional) [#message-optional] Add a custom message that appears in the email body. You can use these variables: * `{signer.name}` - Recipient's name * `{signer.email}` - Recipient's email * `{document.name}` - Document title If left blank, a default message is generated based on the recipient's role and whether the sender is part of an organisation. Enterprise organisations can configure custom sender email addresses from their organisation settings. ## What Recipients Receive [#what-recipients-receive] Email recipient receives Each recipient receives an email containing: * The document title * Your name and email (or team name for organisation accounts) * Your custom message (or a default message) * A button to access the document The email includes a unique signing link specific to that recipient. This link expires once the document is completed or deleted. ## Document Status After Sending [#document-status-after-sending] After sending, your document moves through these statuses: | Status | Meaning | | ------------- | ------------------------------------------------ | | **Pending** | Waiting for recipients to complete their actions | | **Completed** | All recipients have completed their actions | | **Rejected** | A recipient rejected the document | You can track progress from the document detail page, which shows each recipient's signing status. ## Resend to Recipients [#resend-to-recipients] If a recipient hasn't acted on the document, you can send them a reminder: {/* prettier-ignore */} Open the document from your Documents page. Click the actions menu (three dots) and select **Resend**. Check the recipients you want to remind, then click **Send reminder**. Reminders are only available for: * Documents with **Pending** status * Recipients who haven't yet signed The reminder email includes "Reminder:" prefixed to the subject line and uses the same message from the original send. ## Delete a Pending Document [#delete-a-pending-document] To stop a signing process and remove a document: {/* prettier-ignore */} Open the document from your Documents page. Click the actions menu (three dots) and select **Delete**. Type "delete" to confirm, then click **Delete**. Deleting a pending document is irreversible. The document is permanently deleted, all signatures are voided, and recipients receive a cancellation email. ### Delete Behavior by Status [#delete-behavior-by-status] | Status | What happens | | ------------- | -------------------------------------------------------- | | **Draft** | Permanently deleted, no notifications sent | | **Pending** | Permanently deleted, recipients notified of cancellation | | **Completed** | Hidden from your account, recipients keep their copies | ## Document Settings [#document-settings] Access document settings from the **Document Settings** button in the quick actions sidebar of the editor. ### General Settings [#general-settings] | Setting | Description | | -------------------------------- | --------------------------------------------------------------- | | **Language** | The language used for emails sent to recipients | | **Allowed Signature Types** | Which signature methods recipients can use (draw, type, upload) | | **Date Format** | The date format used for date fields | | **Time Zone** | The time zone used for date fields | | **External ID** | A custom identifier for integrating with your external systems | | **Redirect URL** | Where signers are redirected after completing their actions | | **Document Distribution Method** | Email (sends notifications) or None (generates signing links) | ### Security Settings [#security-settings] Control authentication requirements for accessing and signing the document. **Document-level access control:** | Option | Description | | ------------------- | ------------------------------------------------------------------------ | | **Require account** | The recipient must have an account and be signed in to view the document | | **Require 2FA** | The recipient must use two-factor authentication | | **None** | The document can be accessed directly via the signing link | **Recipient Authentication** allows you to require additional verification when a recipient signs a signature field: | Option | Description | | ------------------- | -------------------------------------------- | | **Require passkey** | The recipient must have a passkey configured | | **Require 2FA** | The recipient must have 2FA enabled | | **None** | No additional authentication | You can set authentication at the document level and override it per recipient. Recipient Authentication is an Enterprise feature. See the [Enterprise Edition](/docs/policies/enterprise-edition) for details. ### Attachments [#attachments] Configure document attachments from the right-hand side of the editor header. Attachments are included with the signed document when it is completed. ## Distribution Without Email [#distribution-without-email] If you choose **None** as the distribution method: {/* prettier-ignore */} The document is sent without notifying recipients. You're redirected to the document page where signing links are displayed. Copy the links and share them through your preferred channel (SMS, messaging app, etc.). This is useful when you want to: * Send links via SMS or messaging apps * Embed signing links in your own application * Control the notification timing yourself *** ## See Also [#see-also] * [Create Templates](/docs/users/templates/create) - Save time on documents you send repeatedly * [Direct Links](/docs/users/documents/direct-links) - Let recipients sign without invitations # Upload Documents ## Limitations [#limitations] | Limitation | Value | | ----------------------- | ----------------------------------- | | Supported formats | PDF, DOCX | | Maximum file size | 50MB (configurable for self-hosted) | | Encrypted PDFs | Not supported | | Password-protected PDFs | Not supported | | Legacy `.doc` files | Not supported (convert to DOCX) | Documenso does not support password-protected or encrypted PDF files. Remove encryption before uploading. ## Supported Formats [#supported-formats] Documenso accepts two file formats: * **PDF** (`.pdf`): used as-is. **Recommended.** * **Word** (`.docx`): converted to PDF on the server during upload. The converted PDF is what recipients sign. Other formats (`.doc`, `.odt`, `.rtf`, images) are not supported. Convert them to PDF or DOCX before uploading. **Upload a PDF whenever you can.** DOCX files are converted to PDF using LibreOffice, which is not byte-identical to Microsoft Word. Spacing, line breaks, fonts, and complex elements (tables, charts, headers, footers) can shift in the converted PDF. For the final document to look exactly the way you designed it, export to PDF from Word, Google Docs, or Pages and upload the PDF directly. DOCX support requires the document conversion service. It is enabled on [documenso.com](https://app.documenso.com). Self-hosted instances must [configure it](/docs/self-hosting/configuration/advanced/document-conversion) before DOCX uploads are accepted. ## Upload Methods [#upload-methods] Documents dashboard You can upload documents in two ways: ### Drag and Drop [#drag-and-drop] {/* prettier-ignore */} ### Go to the Documents dashboard [#go-to-the-documents-dashboard] Open your [Documents dashboard](https://app.documenso.com/documents). ### Drag and drop your file [#drag-and-drop-your-file] Drag a PDF or DOCX file from your computer and drop it anywhere on the page. ### Wait for the upload to complete [#wait-for-the-upload-to-complete] The document will process and the editor will open when ready. DOCX files take a few extra seconds while they are converted to PDF. ### File Picker [#file-picker] {/* prettier-ignore */} ### Go to the Documents dashboard [#go-to-the-documents-dashboard-1] Open your [Documents dashboard](https://app.documenso.com/documents). ### Open the upload dialog [#open-the-upload-dialog] Click **Upload Document** in the top right. ### Select your file [#select-your-file] Choose a PDF or DOCX file from your computer. ### Wait for the upload to complete [#wait-for-the-upload-to-complete-1] The document will process and the editor will open when ready. ## DOCX Conversion [#docx-conversion] We always recommend uploading a PDF rather than a DOCX. If you have the original document open in Word, Google Docs, or Pages, export to PDF from there and upload the PDF. The result is guaranteed to match what you see on screen. If you do upload a `.docx` file, Documenso converts it to PDF before adding it to the envelope. The original `.docx` is discarded. Only the converted PDF is stored, signed, and downloaded. Things to keep in mind when uploading DOCX: * **The converted PDF will not be pixel-identical to your Word document.** Conversion uses LibreOffice, which renders most documents faithfully but differs from Microsoft Word in subtle ways. Spacing, font metrics, line breaks, and complex layout features can shift. * **Always review the converted PDF before adding fields or sending.** Open the document in the editor and scroll through every page to confirm it looks the way you expect. * **Form controls are flattened.** Word content controls (drop-downs, date pickers, checkboxes) become static text or graphics. Use Documenso fields for anything that needs to be filled in. * **Fonts not installed on the server fall back to substitutes.** On documenso.com, common fonts (Calibri, Arial, Times New Roman, etc.) are installed. On self-hosted instances, font fidelity depends on the operator's setup. * **Tracked changes and comments are preserved as they appear in Word.** Accept or reject changes and remove comments before uploading if you do not want them in the final document. If the converted PDF does not match what you expect, export the document to PDF from Word, Google Docs, or another tool and upload the PDF directly. ## Uploading Multiple Documents [#uploading-multiple-documents] You can upload multiple files at once to create a single envelope containing multiple documents. The number of files you can upload per envelope depends on your plan. To upload multiple files: * Select multiple PDF or DOCX files when using the file picker, or * Drag and drop multiple files at once You can mix PDF and DOCX files in the same upload. All files become part of the same envelope and share the same recipients and signing workflow. If you need separate signing workflows for each document, upload them individually. ## What Happens After Upload [#what-happens-after-upload] When you upload a document: 1. **Processing**: Documenso validates the PDF and extracts page information 2. **Draft created**: A new document is created in `Draft` status 3. **Editor opens**: You're taken to the document editor to add recipients and fields Document editor overview The document remains in `Draft` status until you send it. You can close the editor and return later - draft documents are saved automatically. ## Troubleshooting [#troubleshooting] Reduce the file size before uploading: * Compress images within the document * Remove unnecessary pages * Use a PDF compression tool (for PDFs) or save with images downsampled (for DOCX) Documenso accepts PDF and DOCX. For other formats (`.doc`, `.odt`, `.rtf`, etc.), export to PDF from your editor (Word, Google Docs, Pages) and upload the PDF. If you are self-hosted and DOCX is rejected, the [document conversion service](/docs/self-hosting/configuration/advanced/document-conversion) is not configured on your instance. The document conversion service was reachable but could not convert the file. Common causes: * The `.docx` file is corrupted. Open it in Word, save a new copy, and try again. * The file uses very unusual fonts or embedded objects that LibreOffice cannot render. * The file is unusually large or complex and exceeded the conversion timeout. If the problem persists, export the document to PDF from Word and upload the PDF directly. The document conversion service is down or temporarily unreachable. Try again in a minute. If you self-host, check the [document conversion service](/docs/self-hosting/configuration/advanced/document-conversion) logs. The PDF is password-protected or encrypted. Open it in a PDF reader, remove the security settings, and save a new copy. Your plan has a monthly document limit. Options: * Wait until your limit resets next month * Upgrade to a higher plan * Delete unused draft documents (this does not restore quota) Check that: * Your internet connection is stable * The file is a valid PDF (try opening it in a PDF reader) * The file is not corrupted *** ## See Also [#see-also] * [Add Recipients](/docs/users/documents/add-recipients) - Specify who needs to sign * [Add Fields](/docs/users/documents/add-fields) - Place signature and other fields on the document * [Send Documents](/docs/users/documents/send) - Send the document and monitor progress # Create an Account ## Choose a Plan [#choose-a-plan] Documenso offers four plans: | Plan | Best for | | ---------- | ---------------------------------------------- | | Free | Trying out document signing (limited features) | | Individual | Solo professionals | | Teams | Small to medium teams needing collaboration | | Platform | Organizations requiring API access and volume | See the [pricing page](https://documen.so/pricing) for feature details and limits. All plans are subject to the [Fair Use Policy](/docs/policies/fair-use). ## Sign Up [#sign-up] {/* prettier-ignore */} ### Go to the registration page [#go-to-the-registration-page] Navigate to [documen.so/free](https://documen.so/free) to create a free account. To start with a paid plan, visit the [pricing page](https://documen.so/pricing) and select your plan. ### Enter your details [#enter-your-details] Provide your name, email address, and create a password. Alternatively, sign up with Google for faster access. Documenso registration form with name, email, and password fields ### Verify your email [#verify-your-email] Check your inbox for a verification email and click the confirmation link. The email is sent from `noreply@documenso.com`. If you don't see the email, check your spam folder. ## Unlock More Features [#unlock-more-features] For custom branding, team collaboration, API access, and other advanced features, you'll need a paid plan. Explore the [pricing page](https://documen.so/pricing) and choose the plan that fits your needs. *** ## Secure Your Account [#secure-your-account] After signing up, secure your account with additional authentication methods. {/* prettier-ignore */} ### Go to Security settings [#go-to-security-settings] Go to **Settings > Security** to enable 2FA for an extra layer of protection. Documenso account security page showing security options ### Enable two-factor authentication [#enable-two-factor-authentication] Click the option to enable two-factor authentication and follow the setup wizard. Dialog to enable two-factor authentication with QR code setup ### Go to Passkeys settings [#go-to-passkeys-settings] Passkeys provide passwordless authentication using your device's biometrics or security key. Go to **Settings > Security** to manage passkeys. Passkeys settings page showing registered passkeys ### Add a passkey [#add-a-passkey] Click **Add Passkey** to register a new passkey for your account. Dialog to add a new passkey with name input field *** ## See Also [#see-also] * [Send Your First Document](/docs/users/getting-started/send-first-document) - Upload a PDF and collect signatures * [Profile Settings](/docs/users/settings/profile) - Update your name and configure your public profile * [Security Settings](/docs/users/settings/security) - Enable two-factor authentication # Getting Started # Send Your First Document ## Prerequisites [#prerequisites] * A [Documenso account](/docs/users/getting-started/create-account). * A PDF document to send for signing. ## Limitations [#limitations] * Maximum file size: 50MB (configurable for self-hosted) * Supported format: PDF only * Maximum 5 documents can be uploaded at once on standard plans ## Send a Document [#send-a-document] {/* prettier-ignore */} ### Upload your document [#upload-your-document] * Go to your [Documents dashboard](https://app.documenso.com/documents) * Click **Upload Document** in the top right * Select your PDF file or drag and drop it into the upload area Documents dashboard showing the upload document button After uploading, you'll be taken to the document editor. Document editor overview with the uploaded document ### Add recipients [#add-recipients] In the editor, add the people who must take an action or receive a copy of the document. * Click **+ Add Signer** * Enter the recipient's email address and name * Select their role: * **Signer** - Must sign the document * **Approver** - Must approve (signature optional) * **Viewer** - Must confirm they viewed it * **CC** - Receives a copy after completion (no action required) Add additional recipients by clicking **+ Add Signer** again. Adding recipients with email, name, and role options Enable **Signing Order** if recipients need to sign in a specific sequence. ### Add signature fields [#add-signature-fields] Navigate to the **Add Fields** section. For each recipient: * Select the recipient in the right sidebar * Drag a **Signature** field onto the document where they should sign * Optionally add other fields like **Name**, **Date**, or **Initials** Fields are color-coded by recipient. Resize fields by dragging their corners. Fields panel showing available field types like Signature, Name, Date, and more At minimum, each signer needs one Signature field assigned to them. ### Send the document [#send-the-document] * Click **Send Document** in the left sidebar (or navigate to the Preview section) * Review that everything looks correct * Click **Send** Your document is now sent. ## What Happens Next [#what-happens-next] 1. **Recipients receive an email** with a link to sign the document 2. **Recipients sign** by clicking the link and completing their assigned fields 3. **You get notified** when each recipient completes their action 4. **Everyone receives a copy** of the fully signed document once all signatures are collected Email notification that recipients receive with a link to sign You can track progress from your Documents dashboard. Click on any document to see which recipients have signed and which are pending. *** ## See Also [#see-also] * [Create Templates](/docs/users/templates/create) - Save time on documents you send repeatedly * [Direct Links](/docs/users/documents/direct-links) - Let recipients sign without email invitations * [Upload and Configure](/docs/users/documents/upload) - Customize document settings for more control # Billing ### Billing and Subscription Management [#billing-and-subscription-management] Organisations handle billing centrally, making it easier to manage: * **Unified Billing**: One subscription covers all teams in the organisation * **Seat Management**: Add or remove seats across all teams automatically (Teams plan) You can change plans, view invoices and manage your subscription from the billing page which is accessible from the organisation settings. A screenshot of the organisation's billing page ## See Also [#see-also] * [Create a Team](/docs/users/organisations/create-team) - Set up a new team * [Team Members](/docs/users/organisations/members) - Manage team members and seats * [Enterprise Edition](/docs/policies/enterprise-edition) - Enterprise licensing options # Create a Team ## Prerequisites [#prerequisites] * An organisation. * Teams exist within an organisations. * Organisation Admin or Manager role to create teams. ## Limitations [#limitations] | Field | Constraint | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Team name** | 3-30 characters | | **Team URL** | 3-30 characters; lowercase letters, numbers, dashes, and underscores only.
Cannot start or end with dashes or underscores.
No consecutive dashes or underscores. | | **Teams** | Maximum number depends on your plan | ## Create a Team [#create-a-team] {/* prettier-ignore */} ### Open the create team dialog [#open-the-create-team-dialog] * Go to your organisation page * Click **Create team** Team creation button on the organisation page ### Enter team details [#enter-team-details] Fill in the required information: * **Team Name**: A display name for your team (3-30 characters) * **Team URL**: A unique identifier used in URLs like `app.documenso.com/t/your-team-url` The Team URL is automatically generated from your team name, but you can customize it. Team creation dialog with name and URL fields ### Configure member access [#configure-member-access] Choose whether to allow all organisation members to access this team: All current and future organisation members can access team documents. Only organisation admins and managers are added initially. You can control member access manually later. You can change member access settings later from team settings. ### Create the team [#create-the-team] Click **Create Team** to finish. You'll be taken to your new team's workspace. ## Team Settings [#team-settings] After creating a team, you can configure additional settings from **Team Settings**: | Setting | Description | | --------------- | ------------------------------------------------------------------------- | | **Team Name** | Update the display name. | | **Team URL** | Change the URL identifier (this changes all team links). | | **Team Avatar** | Upload an image to identify your team. | | **Team Email** | Add an email address for your team (see [Team Email](#team-email) below). | Customize how your team's documents appear to recipients: | Setting | Description | | -------------------------- | -------------------------------------------------------------------------- | | **Enable Custom Branding** | Turn branding on/off, or inherit from organisation settings. | | **Branding Logo** | Upload your logo (max 5MB, JPG, PNG, or WebP) to display on signing pages. | | **Brand Website** | Link your logo to your website. | | **Brand Details** | Additional information shown at the bottom of emails. | Team branding settings can inherit from your organisation. Select "Inherit from organisation" to use organisation-level branding, or configure team-specific branding. | Setting | Description | | ------------------------ | ------------------------------------------- | | **Document Preferences** | Default settings for new documents. | | **Email Preferences** | Customize email notifications. | | **Public Profile** | Configure your team's public presence. | | **API Tokens** | Generate tokens for API access. | | **Webhooks** | Set up integrations with external services. | Document visibility settings in team preferences ## Team Email [#team-email] Adding a team email address enables several capabilities: | Capability | Description | | ----------------------- | ------------------------------------------------------------------------------------------------ | | **Team Inbox** | View signing requests sent to this email address. | | **Send on behalf** | Documents sent from the team appear to come from the team email rather than your personal email. | | **Track documents** | See documents sent from this email in the team's document list. | | **Consistent branding** | Recipients see the team identity rather than individual sender details. | To configure a team email: {/* prettier-ignore */} Go to **Team Settings** > **General**. Enter the email address in the **Team Email** field. Click **Update** to save. The team email is used as the sender identity for documents sent from the team. Recipients will see this email address in signing notifications. *** ## See Also [#see-also] * [Manage Members](/docs/users/organisations/members) - Add team members and configure their roles * [Create Templates](/docs/users/templates/create) - Set up reusable document templates for your team * [Single Sign-On](/docs/users/organisations/single-sign-on) - Configure SSO for your organisation # Email Domains ## Overview [#overview] Email Domains allow you to send emails to recipients from your own domain instead of the default Documenso email address. **Enterprise Only**: Email Domains is only available to Enterprise customers and custom plans ## Creating Email Domains [#creating-email-domains] Before setting up email domains, ensure you have: * An Enterprise subscription * Access to your domain's DNS settings * Access to your Documenso organisation as an admin or manager {/* prettier-ignore */} ### Access email domains settings [#access-email-domains-settings] Navigate to your Organisation email domains settings page and click the "Add Email Domain" button. Email Domains settings page ### Configure DNS records [#configure-dns-records] After adding your domain, Documenso will provide you with the following required DNS records that need to be configured on your domain: * **SPF Record**: Specifies which servers are authorized to send emails from your domain * **DKIM Record**: Provides email authentication and prevents tampering DNS configuration instructions If you already have an SPF record configured, you will need to update it to include Amazon SES as an authorized server instead of creating a new record. Configure these records in your domain's DNS settings according to their specific instructions. ### Verify domain configuration [#verify-domain-configuration] Once you've added the DNS records, return to the Documenso email domains settings page and click the "Verify" button. This will trigger a verification process which will check if the DNS records are properly configured. If successful, the domain will be marked as "Active". Domain verification process Please note that it may take up to 48 hours for the DNS records to propagate. ## Creating Emails [#creating-emails] Once your email domain has been configured, you can create multiple email addresses which your members can use when sending documents to recipients. {/* prettier-ignore */} ### Select the email domain you want to use [#select-the-email-domain-you-want-to-use] Navigate to the email domains settings page and click "Manage" on the domain you want to use. Email Domains settings page ### Add a new email [#add-a-new-email] Click on the "Add Email" button to begin the setup process. Create email ### Use email [#use-email] Once you have added an email, you can configure it to be the default email on either the: * Organisation email preferences page * Team email preferences page When a draft document is created, it will inherit the email configured on the team if set, otherwise it will inherit the email configured in the organisation. You can also configure the email address directly on the document to override the default email if required. ## Notes [#notes] * If you change the default email, it will not retroactively update any existing documents with the old default email. * If the email domain becomes invalid, all emails using that domain will fail to send. ## Troubleshooting [#troubleshooting] * Double-check all DNS record values * Ensure records are added to the correct domain * Wait for DNS propagation (up to 48 hours) * Check domain reputation and blacklist status * Verify SPF, DKIM, and DMARC records * Review bounce and spam reports For additional support with Email Domains configuration, contact our support team at [support@documenso.com](mailto:support@documenso.com). ## See Also [#see-also] * [Email Preferences](/docs/users/organisations/preferences/email) - Configure email notification defaults * [Branding](/docs/users/organisations/preferences/branding) - Customize emails and signing pages * [Enterprise Edition](/docs/policies/enterprise-edition) - Enterprise features including email domains # Groups ## Overview [#overview] Organisation groups lets you manage users across your organisation efficiently. Instead of assigning users to teams one by one, groups allow you to manage access at scale with consistent permissions. ## Key Benefits [#key-benefits] New hires get immediate, appropriate access across all relevant teams. Remove an entire group (e.g. a departing contractor team) and all members lose access simultaneously. The same role is applied consistently across teams. Avoid accidentally giving admin access when member access was intended. Easily track which groups have access to which teams. ## Example: Legal Compliance Team [#example-legal-compliance-team] A legal team needs access across **Sales**, **Marketing**, **HR**, and **Operations**. Instead of adding each person to every team: {/* prettier-ignore */} ### Create the group [#create-the-group] Create a **Legal Compliance** group with **Member** organisation role. ### Add members [#add-members] Add legal team members to the group. ### Assign to teams [#assign-to-teams] Assign the group to the required teams. When someone joins, add them to the group and they will get access to all assigned teams. When they leave, remove them and access is revoked everywhere. ## Getting Started [#getting-started] Go to the **Groups** section in your organisation settings. ### Role Types [#role-types] A global role for all members of the group. Set to **Organisation Member** in most cases, otherwise members get access to all teams by default. The team role you select when assigning the group to a team (e.g. Member, Manager). ### Create a Group [#create-a-group] {/* prettier-ignore */} ### Name the group [#name-the-group] Give it a descriptive name (e.g. "Legal Compliance"). ### Set organisation role [#set-organisation-role] Define the default role for group members. ### Add members [#add-members-1] Include organisation members in the group. Organisation group creation ### Assign a Group to a Team [#assign-a-group-to-a-team] {/* prettier-ignore */} ### Open team settings [#open-team-settings] Go to the team and click **Settings** > **Groups** tab. ### Add the group [#add-the-group] Click **Add groups** and select the group. Members are automatically added to the team. Organisation group assignment ### Manage Groups [#manage-groups] Click **Manage** on a group to view which teams it's assigned to and edit its settings. Organisation group management ## What's Next? [#whats-next] * [Create Your First Team](/docs/users/organisations/create-team) * [Document Preferences](/docs/users/organisations/preferences/document) ## See Also [#see-also] * [Team Members](/docs/users/organisations/members) - Manage individual team members and roles * [Create a Team](/docs/users/organisations/create-team) - Set up a new team in your organisation * [Document Preferences](/docs/users/organisations/preferences/document) - Manage default document settings # Organisations ## Organisation Features [#organisation-features] * **Shared documents**: Organisation members can access and manage documents * **Role-based access**: Control what members can do * **Teams**: Organise members into teams within your organisation * **Centralized billing**: Single subscription for the organisation * **SSO integration**: Use your identity provider *** ## Getting Started [#getting-started] *** ## See Also [#see-also] * [Security Settings](/docs/users/settings/security) - Configure 2FA and passkeys # Team Members ## Organisation Roles [#organisation-roles] Organisation members have different permission levels that determine what they can do across the organisation. | Role | Manage Settings | Manage Teams | Manage Members | Billing | Delete Organisation | | ----------- | --------------- | ------------ | -------------- | ------- | ------------------- | | **Owner** | Yes | Yes | Yes | Yes | Yes | | **Admin** | Yes | Yes | Yes | Yes | Yes | | **Manager** | Yes | Yes | Yes | No | No | | **Member** | No | No | No | No | No | Organisation Admins and Managers automatically receive Team Admin access to all teams in the organisation. ## Transferring Organisation Ownership [#transferring-organisation-ownership] Organisation ownership cannot be transferred through the regular organisation settings. Only a Documenso instance administrator can transfer ownership through the admin panel. If you are using Documenso Cloud, contact support to request an ownership transfer. If you are self-hosting, an instance administrator can follow the steps below. The target user must already be a member of the organisation. {/* prettier-ignore */} Navigate to **Admin > Organisations** and select the organisation. In the **Organisation Members** table, find the target member and click **Update role**. Select **Owner** from the role dropdown and click **Update**. After the transfer: * The new owner is promoted to Admin if they previously held a lower role (Manager or Member). * The previous owner retains their Admin role and remains a member of the organisation. * Only one user can be the owner at a time. The current owner cannot be demoted below Admin. Transfer ownership to another member first. ## Team Member Roles [#team-member-roles] Teams have three roles with different permission levels: | Role | Permissions | | ----------- | ------------------------------------------------------------------------------------------------------ | | **Admin** | Full access to team settings, can manage all members, delete the team, and control document visibility | | **Manager** | Can manage team members (at or below their role level) and access manager-level documents | | **Member** | Can access team documents shared with all members | ### Role hierarchy [#role-hierarchy] Higher roles can manage users at or below their level: * Admins can add, update, or remove **Admins**, **Managers**, and **Members** * Managers can add, update, or remove **Managers** and **Members** * Members cannot manage other team members ## Adding Team Members [#adding-team-members] Before adding someone to a team, they must first be a member of your organisation. ### Add existing organisation members [#add-existing-organisation-members] {/* prettier-ignore */} Go to your team settings and click **Add members**. Select organisation members from the list and click **Next**. Assign a team role to each member, then click **Add Members**. If you can't find someone in the member list, they need to be invited to your organisation first. You can do this directly from the team member dialog. ### Invite new organisation members [#invite-new-organisation-members] If the person you want to add isn't in your organisation yet: {/* prettier-ignore */} In the **Add members** dialog, click **Invite them to the organisation first**. Enter their email address and organisation role, then click **Send invite**. Once they accept, return to add them to the team. ### Bulk invite via CSV [#bulk-invite-via-csv] You can invite multiple members at once by uploading a CSV file. The CSV should have the following structure: | Email address | Role | | ------------------------------------------------- | ------- | | [admin@example.com](mailto:admin@example.com) | Admin | | [manager@example.com](mailto:manager@example.com) | Manager | | [member@example.com](mailto:member@example.com) | Member | ## Accepting Invitations [#accepting-invitations] When someone invites you to an organisation: {/* prettier-ignore */} Check your email for the invitation and click the link. Sign in or create a Documenso account. Accept the invitation. After joining the organisation, a team admin or manager can add you to specific teams. ## Changing Member Roles [#changing-member-roles] To change a team member's role: {/* prettier-ignore */} Go to your team settings and find the member in the list. Click the actions menu (three dots) and select **Update role**. Choose the new role and click **Update**. You can only change roles for members at or below your own role level. For example, managers cannot change an admin's role. ## Removing Members [#removing-members] To remove someone from a team: {/* prettier-ignore */} Go to your team settings and find the member in the list. Click the actions menu (three dots) and select **Remove from team**. Confirm the removal. Removing someone from a team doesn't remove them from the organisation. They can be re-added to the team later. ## Member Limits [#member-limits] Member limits depend on your plan. The Platform plan includes unlimited members but is limited to a single team. The Teams plan includes 5 organisation members, with additional members charged per seat. | Plan | Included Members | Teams | Additional Members | | -------------- | ---------------- | ------ | ------------------ | | **Platform** | Unlimited | 1 | N/A | | **Teams** | 5 | Custom | Per-seat pricing | | **Enterprise** | Custom | Custom | Per agreement | When you invite new members on a seat-based plan, your billing automatically adjusts. Check your organisation's billing settings to see your current member count and limits. ## Document Visibility [#document-visibility] Team documents have visibility settings that control which roles can access them: | Visibility | Who can see | | --------------------- | ------------------- | | **Admin only** | Only team admins | | **Manager and above** | Admins and managers | | **Everyone** | All team members | Admins can set default visibility for new documents in team settings. *** ## See Also [#see-also] * [Single Sign-On](/docs/users/organisations/single-sign-on) - Configure SSO for your organisation * [Create a Team](/docs/users/organisations/create-team) - Set up additional teams # Overview ## What are Organisations? [#what-are-organisations] Organisations are the top-level entity in Documenso's hierarchy. Each organisation can contain multiple teams, and each team can have multiple members. Organisations diagram This structure provides: | Benefit | Description | | -------------------------- | ----------------------------------------------------------------- | | **Centralized Management** | Control multiple teams from a single organisational dashboard. | | **Unified Billing** | Manage billing and subscriptions at the organisation level. | | **Access Control** | Define roles and groups across the entire organisation. | | **Group Management** | Create custom groups to organise members and control team access. | | **Global Settings** | Apply consistent settings across all teams in your organisation. | ## Create a New Organisation [#create-a-new-organisation] You can create multiple organisations, but each organisation is billed separately. {/* prettier-ignore */} ### Open organisation creation [#open-organisation-creation] Navigate to the organisation section and click **Create Organisation**. Create organisation in Documenso dashboard ### Select your plan [#select-your-plan] Choose a plan for your new organisation. If you want to upgrade an existing organisation, go to your organisation's billing settings. ### Name your organisation [#name-your-organisation] Enter an **Organisation Name** as the display name for your organisation. Once created, you can create teams to organise your work. Each team operates independently but inherits organisation-level settings and branding. ## Organisation vs Team Settings [#organisation-vs-team-settings] Teams inherit settings from their parent organisation by default. You can override these at the team level: | Setting | Organisation Level | Team Level | | ------------------------ | -------------------------- | ----------------------- | | **Branding** | Sets default for all teams | Can override or inherit | | **Document Preferences** | Sets default for all teams | Can override or inherit | | **Email Preferences** | Sets default for all teams | Can override or inherit | | **Document Visibility** | Sets default for all teams | Can override or inherit | | **Billing** | Managed centrally | Not applicable | | **SSO** | Configured centrally | Not applicable | | **Members** | Managed at org level | Assigned per team | ## Best Practices [#best-practices] Leverage groups to simplify permission management across teams. Configure document preferences, branding, and email settings at the organisation level for consistency. Organisation Admins and Managers automatically receive Team Admin access to all teams. *** ## See Also [#see-also] * [Create a Team](/docs/users/organisations/create-team) - Set up a team within your organisation * [Team Members](/docs/users/organisations/members) - Manage member roles at the organisation and team level * [Groups](/docs/users/organisations/groups) - Organise members into groups for bulk team access * [Billing](/docs/users/organisations/billing) - Manage organisation billing and subscriptions * [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings # API Tokens ## Overview [#overview] API tokens allow external applications to access your Documenso account programmatically. Tokens can be scoped to your personal account or to a specific team. For detailed instructions on creating and managing API tokens, see the [Authentication](/docs/developers/getting-started/authentication) guide in the developer documentation. ## Best Practices [#best-practices] * **Use separate tokens per integration** — if one is compromised, revoke only that token * **Set expiration dates** — shorter lifespans reduce risk if a token is leaked * **Use the minimum required scope** — only grant access to what the integration needs *** ## See Also [#see-also] * [Authentication](/docs/developers/getting-started/authentication) - API authentication guide * [Security Settings](/docs/users/settings/security) - Manage passwords, 2FA, and passkeys * [Sessions & Activity](/docs/users/settings/sessions) - Monitor account activity # Delete Account Account deletion is permanent and irreversible. Your account, signatures, and personal data will be permanently removed, and any active subscription will be cancelled. How your organisations and documents are handled is explained below. ## Before Deleting [#before-deleting] * Download any documents you need to keep * Disable two-factor authentication (required before deletion) ## Delete Your Account [#delete-your-account] {/* prettier-ignore */} Go to **Settings** > **Profile** and scroll to the **Delete Account** section. Click **Delete Account**. Type your email address to confirm and click **Confirm Deletion**. If you have two-factor authentication enabled, you must disable it before deleting your account. ## What Happens to Your Organisations [#what-happens-to-your-organisations] When you delete your account, the organisations you **own** are permanently deleted along with all of their teams. If an owned organisation has an active subscription, it is scheduled for cancellation at the end of the current billing period. Organisations that you are only a **member** of are not deleted. You are simply removed from them, and the organisation continues to operate as normal. ## What Happens to Your Documents [#what-happens-to-your-documents] The way your documents and templates are handled depends on whether you owned the organisation they belong to: * **Organisations you owned** — Completed and in-progress documents are retained in an anonymized form (reassigned to an internal system account) so the other parties keep their records. Draft documents and templates are permanently removed. * **Organisations you were a member of** — Your documents and templates are transferred to the organisation owner, so they remain accessible to the organisation after you leave. Documents that are retained in anonymized form are no longer associated with your account and cannot be recovered or accessed by you after deletion. Download anything you need to keep beforehand. *** ## See Also [#see-also] * [Profile Settings](/docs/users/settings/profile) - Manage your profile and signature * [Security Settings](/docs/users/settings/security) - Manage 2FA and passkeys # Settings *** ## See Also [#see-also] * [Organisations](/docs/users/organisations) - Manage your organisation, branding, and email domains # Profile ## Avatar [#avatar] Your avatar appears on documents you send and in your public profile. {/* prettier-ignore */} Go to **Settings** > **Profile**. Click **Upload Avatar** next to your current avatar and select an image (PNG or JPG, max 1MB). To remove your avatar, hover over the image and click **Remove**. ## Name [#name] Your name appears on documents and email notifications sent to recipients. {/* prettier-ignore */} Go to **Settings** > **Profile**. Update the **Full Name** field and click **Update profile**. ## Email [#email] Your email address is shown as read-only on the profile page. To change your email address, contact support. ## Signature [#signature] Your signature is used as the default when you sign documents. You can create your signature using three methods: {/* prettier-ignore */} Go to **Settings** > **Profile** and click on the **Signature** field to open the signature editor. Choose one of three methods: | Method | Description | | ---------- | ---------------------------------------------------------- | | **Draw** | Use your mouse or touchscreen to draw your signature | | **Type** | Enter your name and select from generated signature styles | | **Upload** | Upload an image of your signature | Click **Next** to confirm your signature, then click **Update profile** to save. When signing documents, you can create a different signature for that specific document without changing your default signature. *** ## See Also [#see-also] * [Public Profile](/docs/users/settings/public-profile) - Share your profile and link templates * [Security Settings](/docs/users/settings/security) - Manage passwords, 2FA, and passkeys * [Delete Account](/docs/users/settings/delete-account) - Permanently delete your account # Public Profile Your public profile allows others to view your information and access templates you share publicly. Your profile is accessible at: ``` https://app.documenso.com/p/ ``` Enable public profile ## Configure Your Public Profile [#configure-your-public-profile] {/* prettier-ignore */} Go to **Settings** > **Public Profile**. Toggle **Show/Hide** to control profile visibility. Set your **Public profile URL** (personal accounts only) and add a **Bio** if desired. Click **Update** to save. Public profile settings ## Link Templates to Your Public Profile [#link-templates-to-your-public-profile] Display templates on your public profile so visitors can sign documents directly: {/* prettier-ignore */} Go to **Settings** > **Public Profile**. Click **Link template** in the Templates section and select templates that have direct links enabled. Linked templates will appear on your public profile page. Only templates with direct links enabled can be added to your public profile. See [Direct Links](/docs/users/documents/direct-links) to learn more. A free Documenso account gives you 5 free signatures per month. *** ## See Also [#see-also] * [Profile Settings](/docs/users/settings/profile) - Update your name, email, and signature * [Direct Links](/docs/users/documents/direct-links) - Share signing links without email invitations # Security ## Change Your Password [#change-your-password] If you signed up with email and password (not Google or another provider), you can change your password at any time. {/* prettier-ignore */} Click your avatar in the top right corner. Go to **Settings**. Open the **Security** tab. Enter your current password, new password, and confirmation, then click **Update password**. The password change form only appears if you have a password-based account. If you signed up with Google or another provider, you won't see this option. ## Two-Factor Authentication (2FA) [#two-factor-authentication-2fa] Two-factor authentication (2FA) and passkeys are used for high-security and high-compliance signatures in addition to account login. Two-factor authentication adds a second layer of security by requiring a code from an authenticator app when you sign in or sign documents. Enable two-factor authentication ### Enable 2FA [#enable-2fa] {/* prettier-ignore */} ### Open 2FA setup [#open-2fa-setup] Go to **Settings**, open the **Security** tab, find the Two factor authentication section, and click **Enable 2FA**. ### Scan the QR code [#scan-the-qr-code] Open your authenticator app (Google Authenticator, Authy, 1Password, etc.) and scan the QR code. If you can't scan, reveal the setup key and enter it manually. ### Verify and save [#verify-and-save] Enter the 6-digit code from your app, click **Enable**, then save your recovery codes (see [Recovery Codes](#recovery-codes) below). Logging in with Google does not require a 2FA code. As an authentication provider, your Google account is considered secure (e.g. you can configure 2FA on your Google account separately). Logging in with a passkey does not require a 2FA code. Passkeys are considered two-factor by design — the passkey itself is the first factor, and access to the device that holds it is the second factor. ### Disable 2FA [#disable-2fa] {/* prettier-ignore */} Go to **Settings**. Open the **Security** tab. Click **Disable 2FA**, enter a code from your authenticator app, then click **Disable**. ## Recovery Codes [#recovery-codes] Recovery codes let you access your account if you lose access to your authenticator app. When you enable 2FA, Documenso generates a set of backup codes. ### View Recovery Codes [#view-recovery-codes] {/* prettier-ignore */} Go to **Settings**, open the **Security** tab. Click **View Codes** in the Recovery codes section. Enter a code from your authenticator app to view your recovery codes. ### Store Recovery Codes Safely [#store-recovery-codes-safely] * Click **Download** to save them as a text file * Store them in a secure location (password manager, safe, etc.) * Each code can only be used once If you lose both your authenticator app and your recovery codes, you will be locked out of your account. Store your recovery codes securely. ## Passkeys [#passkeys] Passkeys provide passwordless authentication using biometrics, hardware security keys, or password managers. You can use passkeys to sign in to Documenso and to authenticate when signing documents. Passkeys management page ### Add a Passkey [#add-a-passkey] {/* prettier-ignore */} ### Open passkeys [#open-passkeys] Go to **Settings**, open the **Security** tab, then click **Manage passkeys**. ### Add and name your passkey [#add-and-name-your-passkey] Click **Add passkey**, enter a name (e.g. "MacBook Touch ID", "YubiKey"), then follow your browser's prompts to complete registration. Add passkey dialog ### Manage Passkeys [#manage-passkeys] From the passkeys page, you can: * **Rename** a passkey by clicking the edit button * **Delete** a passkey by clicking the delete button Passkeys are device-specific. If you use multiple devices, add a passkey on each one. *** ## See Also [#see-also] * [Sessions & Activity](/docs/users/settings/sessions) - View active sessions and security activity * [API Tokens](/docs/users/settings/api-tokens) - Manage programmatic access tokens * [Profile](/docs/users/settings/profile) - Update your name, email, and signature * [Delete Account](/docs/users/settings/delete-account) - Permanently delete your account # Sessions & Activity ## Active Sessions [#active-sessions] View and manage all devices currently logged into your account. ### View Sessions [#view-sessions] Go to **Settings**, open the **Security** tab, then click **Manage sessions**. The sessions table shows: | Column | Description | | ----------- | ------------------------------ | | Device | Browser and operating system | | IP Address | The IP address of the session | | Last Active | When the session was last used | | Created | When the session started | Your current session is marked with a "Current" badge. ### Revoke a Session [#revoke-a-session] Find the session in the list and click **Revoke**. Click **Log out all** at the top of the page and confirm. Your current session stays active; all others are terminated. ## Security Activity [#security-activity] Monitor recent security-related events on your account. Go to **Settings**, open the **Security** tab, then click **View activity**. The activity log shows events such as: | Event | Description | | ---------------- | ------------------------------------- | | Sign-in attempts | Login attempts to your account | | Password changes | When your password was changed | | 2FA changes | When 2FA was enabled or disabled | | Session changes | When sessions were created or revoked | ## Linked Accounts [#linked-accounts] View and manage login methods connected to your account (Google, email/password, etc.). Go to **Settings**, open the **Security** tab, and click **Manage linked accounts**. To remove a login method: {/* prettier-ignore */} Find the provider in the list under **Manage linked accounts**. Click **Unlink** and confirm. Ensure you have at least one login method available before unlinking an account. *** ## See Also [#see-also] * [Security Settings](/docs/users/settings/security) - Manage passwords, 2FA, and passkeys * [API Tokens](/docs/users/settings/api-tokens) - Manage programmatic access tokens # Create Templates ## Overview [#overview] Templates are reusable document configurations that save you from recreating the same setup every time you send a similar document. A template stores your PDF, recipient roles, field placements, and settings so you can generate new documents with a few clicks. Use templates when you: * Send the same type of document regularly (contracts, NDAs, onboarding forms) * Need consistent field placement across documents * Want to pre-configure recipients and signing workflows * Plan to use [direct links](/docs/users/documents/direct-links) for public signing ## Create a Template from Scratch [#create-a-template-from-scratch] {/* prettier-ignore */} ### Go to templates [#go-to-templates] Navigate to **Templates** in the sidebar and click **Upload Template**. Templates page ### Upload your document [#upload-your-document] Select one or more PDF files to upload. If you select multiple files, they're combined into a single template. After uploading, you'll enter the template editor where you can configure recipients, fields, and settings. Template with uploaded document ### Add recipients [#add-recipients] Add the people who will interact with documents created from this template. For each recipient, specify: * **Email**: Use a real email if the recipient is always the same person, or a placeholder email (like `client@placeholder.com`) if it changes each time * **Name**: Optional, but helps identify recipients * **Role**: What the recipient needs to do (Signer, Approver, Viewer, CC) Template recipients ### Add fields [#add-fields] Drag fields from the field palette onto your document and assign them to recipients. At minimum, each signer needs one signature field. See [Add Fields](/docs/users/documents/add-fields) for detailed guidance on field types and configuration. ### Save the template [#save-the-template] Click **Save Template** to finish. Your template appears in the Templates list, ready to use. *** ## Placeholder Recipients [#placeholder-recipients] Templates often need placeholder recipients - people whose actual email addresses you won't know until you use the template. For example, a sales contract template might have: * **You** (your real email) - Always signs as the company representative * **Client** (placeholder) - Different person for each contract ### Setting up placeholders [#setting-up-placeholders] Use any email format that's clearly a placeholder: * `client@placeholder.com` * `signer1@example.com` * `recipient@documenso.com` When you [use the template](/docs/users/templates/use), you'll replace these placeholders with actual recipient emails. ### When to use real emails [#when-to-use-real-emails] Use real email addresses when the recipient is always the same person: * Your own email for documents you always sign * A team member who reviews all contracts * A compliance officer who needs to approve every agreement *** ## Template Settings [#template-settings] Click the settings icon in the template editor to configure these options: ### Title [#title] The template name shown in your Templates list. Use a descriptive name like "Standard NDA" or "Freelance Contract - Hourly" so you can find it quickly. The title also becomes the default document name when you create documents from the template. Recipients see this name in their signing invitation. ### External ID [#external-id] An optional identifier for integrating with external systems. Use this to: * Match templates to records in your CRM or contract management system * Track which template generated a document in your analytics * Reference templates in API calls The external ID has no effect on document signing - it's purely for your reference. ### Global settings [#global-settings] Additional options that apply to all documents created from this template: | Setting | Description | | ------------ | ---------------------------------------------------------------- | | Date format | How dates appear in Date fields (e.g., MM/DD/YYYY or DD/MM/YYYY) | | Time zone | Time zone used for date calculations | | Redirect URL | Where to send recipients after they finish signing | | Language | Language for the signing interface and emails | *** ## Template Visibility [#template-visibility] All templates are created in a team context. By default, templates are **Private** and only visible to members of the owning team. If your organisation has multiple teams, you can set a template's type to **Organisation** to share it across all teams. See [Organisation Templates](/docs/users/templates/organisation-templates) for details. *** ## Edit a Template [#edit-a-template] To modify an existing template: {/* prettier-ignore */} Go to **Templates** and find the template you want to edit. Click on the template to open it in the editor. Make your changes to recipients, fields, or settings. Click **Save Template**. Changes only affect future documents created from the template. Documents already created from this template are not updated. *** ## Duplicate a Template [#duplicate-a-template] To create a copy of an existing template: {/* prettier-ignore */} Go to **Templates** and find the template. Click the three-dot menu and select **Duplicate**. The copy opens in the editor; make any changes and save with a new name. This is useful when you need a variation of an existing template without modifying the original. *** ## Delete a Template [#delete-a-template] To delete a template: {/* prettier-ignore */} Go to **Templates** and find the template. Click the three-dot menu and select **Delete**. Confirm the deletion. Deleting a template is permanent. Documents already created from the template are not affected, but you won't be able to create new documents from it. *** ## See Also [#see-also] * [Use a Template](/docs/users/templates/use) - Create documents from your template * [Direct Links](/docs/users/documents/direct-links) - Share a signing link without knowing recipient emails * [API: Templates](/docs/developers/api/templates) - Create and use templates programmatically # Templates ## Why Use Templates? [#why-use-templates] * **Save time**: Don't recreate the same document structure repeatedly * **Consistency**: Ensure fields are placed correctly every time * **Efficiency**: Pre-configure recipients and settings *** ## Template Workflow [#template-workflow] *** ## See Also [#see-also] * [Documents](/docs/users/documents) - Sending individual documents * [API Templates](/docs/developers/api/templates) - Create templates via API # Organisation Templates ## Overview [#overview] Organisation templates are templates shared across all teams within the same organisation. Any team in the organisation can browse and use them to create documents, but only the owning team can edit or delete them. This is useful when you have standardised documents that multiple teams need to use, such as company-wide NDAs, onboarding agreements, or compliance forms. ## Requirements [#requirements] The Organisation template type is available when your organisation has **two or more teams**. If your organisation has only one team, the option does not appear. ## Template Types [#template-types] | Type | Who can see it | Who can edit it | Who can use it | | ---------------- | -------------------------- | ---------------- | ---------------------- | | **Private** | Members of the owning team | Owning team | Owning team | | **Organisation** | All teams in the org | Owning team only | All teams in the org | | **Public** | Anyone with the link | Owning team | Anyone via direct link | ## Set a Template as Organisation [#set-a-template-as-organisation] {/* prettier-ignore */} ### Open template settings [#open-template-settings] Navigate to **Templates**, open the template you want to share, and click **Edit Template** to open the editor. Then open the settings dialog. ### Change the template type [#change-the-template-type] In the **Template type** dropdown, select **Organisation**. This option only appears if your organisation has at least two teams. ### Save [#save] Click **Save** to apply the change. The template is now visible to all teams in your organisation. You can also set the template type to Organisation when creating a new template. The type dropdown appears in the template settings step. ## Browse Organisation Templates [#browse-organisation-templates] {/* prettier-ignore */} ### Open the templates page [#open-the-templates-page] Navigate to **Templates** in the sidebar. ### Switch to the Organisation tab [#switch-to-the-organisation-tab] Click the **Organisation** tab above the template list. This tab only appears for non-personal organisations. The Organisation tab shows all organisation templates from every team in your organisation, including your own. Templates from other teams display the owning team's name next to the template type. ## Use an Organisation Template [#use-an-organisation-template] Any team member in the organisation can create documents from an organisation template, even if the template belongs to a different team. {/* prettier-ignore */} Find the template in the **Organisation** tab or click through from the template detail page. Click **Use Template** and fill in the recipient details. The document is created under your team, not the template's owning team. See [Use Templates](/docs/users/templates/use) for details on creating documents from templates. ## Editing and Permissions [#editing-and-permissions] Only members of the team that owns the template can edit or delete it. When viewing an organisation template from another team: * The **Edit Template**, **Direct Link**, and **Bulk Send** controls are hidden * The recipients section is read-only * The **Use Template** button is available To modify a template owned by another team, contact that team's members or ask an organisation admin to make changes. ## Visibility [#visibility] Organisation templates respect the same visibility settings as other templates. A template's visibility determines which team roles can access it: | Visibility | Who can access | | --------------------- | ----------------------------------------- | | **Everyone** | All team members (Admin, Manager, Member) | | **Manager and above** | Admins and Managers only | | **Admin** | Admins only | This applies to both the owning team and other teams in the organisation. A Member-role user on any team cannot see an organisation template set to Admin visibility. ## Reverting to Private [#reverting-to-private] To stop sharing a template across the organisation, change the template type back to **Private** in the template settings. The template will only be visible to the owning team. Documents already created from the template are not affected. *** ## See Also [#see-also] * [Create Templates](/docs/users/templates/create) - Build reusable templates * [Use Templates](/docs/users/templates/use) - Create documents from templates * [Organisations](/docs/users/organisations) - Managing organisations and teams # Use Templates ## Create a Document from a Template [#create-a-document-from-a-template] {/* prettier-ignore */} ### Open the template [#open-the-template] Navigate to **Templates** in the sidebar and find the template you want to use. Click on the template to open its detail page, or click the three-dot menu and select **Use Template**. Templates page ### Click use template [#click-use-template] From the template detail page, click the **Use Template** button. A dialog opens where you configure the document. Using a template dialog ### Fill in recipient details [#fill-in-recipient-details] For each recipient defined in the template, enter their: * **Email** (required): The recipient's email address * **Name** (optional): The recipient's display name If the template has pre-filled recipient information, those fields appear with the existing values. You can modify them or leave them as-is. ### Choose how to create the document [#choose-how-to-create-the-document] Creates the document without sending. You can review and edit it before sending. Leave the **Send document** checkbox unchecked. Creates the document and immediately sends it to recipients. Check the **Send document** checkbox to send immediately. ### New document [#new-document] Click **Create as draft** or **Create and send** depending on what you want to do. You are then redirected to the new document. ## Upload a Custom Document [#upload-a-custom-document] You can replace the template's PDF with a different document when creating from a template. This keeps all the field positions and recipient settings but uses your uploaded file. {/* prettier-ignore */} In the Use Template dialog, check **Upload custom document**. Click **Upload** next to each document you want to replace and select a PDF from your computer. Complete the rest of the form and create the document. The uploaded document must be a PDF. Field positions are preserved from the template, so the new document should have a similar layout. ## Sequential Signing [#sequential-signing] If the template uses sequential signing, recipients receive the document in order. The signing order displays next to each recipient in the dialog and cannot be changed when using the template. To modify the signing order, edit the template itself before creating documents from it. ## Distribution Methods [#distribution-methods] How recipients receive the document depends on the template's distribution method: | Distribution method | What happens | | ------------------- | ----------------------------------------------------------- | | **Email** | Recipients receive an email with a link to sign | | **None** | No emails sent. You receive signing links to share manually | When using a template with no email distribution and selecting **Create as pending**, you're redirected to the document page where signing links are displayed for you to copy and share. ## Template vs Document Differences [#template-vs-document-differences] When you create a document from a template, the document becomes independent. Changes you make to the document don't affect the template, and changes to the template don't affect existing documents. | What you can change | Template | Document created from template | | ------------------- | -------- | -------------------------------- | | Recipients | Yes | Yes (before sending) | | Fields | Yes | Yes (before sending) | | PDF content | Yes | Only at creation (custom upload) | | Settings | Yes | Yes (before sending) | Documents created from a template are tracked on the template's detail page under **Documents created from template**. ## Bulk Send [#bulk-send] To create multiple documents from one template with different recipients, use bulk send: {/* prettier-ignore */} Go to the template detail page or templates list. Click the three-dot menu and select **Bulk send**. Upload a CSV file with recipient information. Each row creates a separate document. This is useful for sending the same document to many people, such as offer letters or agreements. *** ## See Also [#see-also] * [Create a Template](/docs/users/templates/create) - Build reusable templates from your documents * [Direct Links](/docs/users/documents/direct-links) - Let recipients sign without knowing their email upfront * [Send Documents](/docs/users/documents/send) - Sending options and email customization # Editor In addition to embedding signing, Documenso supports embedded editor. It allows your users to create and edit documents, templates, and envelopes without leaving your application. Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing). Contact sales for access. ## Versions [#versions] Embedded editor is available in two versions: * **[V1 Editor](/docs/developers/embedding/editor/v1)** — Works with V1 Documents and Templates. * **[V2 Editor](/docs/developers/embedding/editor/v2)** — Works with Envelopes, which are the unified model for documents and templates. ### Comparison [#comparison] | Aspect | V1 | V2 | | ----------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | Entity model | Documents and Templates (separate) | Envelopes (unified, can be documents or templates) | | API compatibility | V1 Documents/Templates API | V2 Envelopes API | | Customization | 6 simple boolean flags | Rich structured settings with sections (general, settings, actions, envelope items, recipients) | *** ## Presign Tokens [#presign-tokens] Before using any editor component, obtain a presign token from your backend: ``` POST /api/v2/embedding/create-presign-token ``` This endpoint requires your Documenso API key. The token has a default expiration of 1 hour. See the [API documentation](https://openapi.documenso.com/reference#tag/embedding) for full details. Presign tokens should be created server-side. Never expose your API key in client-side code. *** ## Next Steps [#next-steps] * [V1 Editor](/docs/developers/embedding/editor/v1) — Create and edit documents and templates using V1 components * [V2 Editor](/docs/developers/embedding/editor/v2) — Create and edit envelopes using V2 components * [CSS Variables](/docs/developers/embedding/css-variables) — Customize the appearance of embedded components * [SDKs](/docs/developers/embedding/sdks) — Framework-specific SDK documentation # V1 Editor V1 editor components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application. Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing). Contact sales for access. ## Components [#components] The SDK provides four V1 editor components: | Component | Purpose | | ----------------------- | ----------------------- | | `EmbedCreateDocumentV1` | Create new documents | | `EmbedCreateTemplateV1` | Create new templates | | `EmbedUpdateDocumentV1` | Edit existing documents | | `EmbedUpdateTemplateV1` | Edit existing templates | *** ## Presign Tokens [#presign-tokens] All editor components require a **presign token** for authentication. See the [Editor overview](/docs/developers/embedding/editor) for details on obtaining presign tokens. A presigned token is NOT an API token *** ## Creating Documents [#creating-documents] ```jsx import { EmbedCreateDocumentV1 } from '@documenso/embed-react'; const DocumentCreator = ({ presignToken }) => { return (
{ console.log('Document created:', data.documentId); console.log('External ID:', data.externalId); }} />
); }; ``` *** ## Creating Templates [#creating-templates] ```jsx import { EmbedCreateTemplateV1 } from '@documenso/embed-react'; const TemplateCreator = ({ presignToken }) => { return (
{ console.log('Template created:', data.templateId); }} />
); }; ``` *** ## Editing Documents [#editing-documents] ```jsx import { EmbedUpdateDocumentV1 } from '@documenso/embed-react'; const DocumentEditor = ({ presignToken, documentId }) => { return (
{ console.log('Document updated:', data.documentId); }} />
); }; ``` *** ## Editing Templates [#editing-templates] ```jsx import { EmbedUpdateTemplateV1 } from '@documenso/embed-react'; const TemplateEditor = ({ presignToken, templateId }) => { return (
{ console.log('Template updated:', data.templateId); }} />
); }; ``` *** ## Props [#props] ### All Editor Components [#all-editor-components] | Prop | Type | Required | Description | | ------------------ | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `presignToken` | `string` | Yes | Authentication token from your backend | | `externalId` | `string` | No | Your reference ID to link with the document or template | | `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` | | `css` | `string` | No | Custom CSS string (Platform Plan) | | `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) | | `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) | | `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) | | `className` | `string` | No | CSS class for the iframe | | `features` | `object` | No | Feature toggles for the editor experience | ### Update Components Only [#update-components-only] | Prop | Type | Required | Description | | ---------------- | --------- | -------- | ---------------------------------------------------------- | | `documentId` | `number` | Yes | The document ID to edit (for document update component) | | `templateId` | `number` | Yes | The template ID to edit (for template update component) | | `onlyEditFields` | `boolean` | No | Restrict editing to fields only, skipping recipient config | *** ## Feature Toggles [#feature-toggles] Customize what options are available in the editor experience: ```jsx ``` *** ## Event Callbacks [#event-callbacks] All creation callbacks receive: | Field | Type | Description | | ---------------------------- | -------- | --------------------------------------- | | `documentId` or `templateId` | `number` | The ID of the created or updated item | | `externalId` | `string` | Your external reference ID, if provided | *** ## Field-Only Editing [#field-only-editing] Restrict users to editing fields only, skipping recipient configuration: ```jsx { console.log('Fields updated:', data.documentId); }} /> ``` *** ## Complete Integration Example [#complete-integration-example] This example shows a full workflow where users create a document and then edit it: ```tsx import { useState } from 'react'; import { EmbedCreateDocumentV1, EmbedUpdateDocumentV1 } from '@documenso/embed-react'; const DocumentManager = ({ presignToken }) => { const [documentId, setDocumentId] = useState(null); const [mode, setMode] = useState('create'); if (mode === 'success') { return (

Document updated successfully

); } if (mode === 'edit' && documentId) { return (
{ console.log('Document updated:', data.documentId); setMode('success'); }} />
); } return (
{ console.log('Document created:', data.documentId); setDocumentId(data.documentId); setMode('edit'); }} />
); }; ``` *** ## Additional Props [#additional-props] Pass extra props to the iframe for testing experimental features: ```jsx ``` Presign tokens expire after 1 hour by default. You can customize this duration based on your security requirements. Generate fresh tokens for each session and avoid caching them on the client side. *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props * [V2 Editor](/docs/developers/embedding/editor/v2) - V2 envelope editor * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Documents API](/docs/developers/api/documents) - Create documents via API * [Templates API](/docs/developers/api/templates) - Create templates via API # V2 Editor V2 editor components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API. Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing). Contact sales for access. ## Components [#components] The SDK provides two V2 editor components: | Component | Purpose | | --------------------- | ----------------------- | | `EmbedCreateEnvelope` | Create new envelopes | | `EmbedUpdateEnvelope` | Edit existing envelopes | *** ## Presign Tokens [#presign-tokens] All editor components require a **presign token** for authentication. See the [Editor overview](/docs/developers/embedding/editor) for details on obtaining presign tokens. A presigned token is NOT an API token *** ## Creating Envelopes [#creating-envelopes] Use `EmbedCreateEnvelope` to embed envelope creation. The `type` prop determines whether the envelope is created as a document or template. ```jsx import { EmbedCreateEnvelope } from '@documenso/embed-react'; const EnvelopeCreator = ({ presignToken }) => { return (
{ console.log('Envelope created:', data.envelopeId); console.log('External ID:', data.externalId); }} />
); }; ``` To create a template instead of a document, set `type` to `"TEMPLATE"`: ```jsx { console.log('Template envelope created:', data.envelopeId); }} /> ``` *** ## Editing Envelopes [#editing-envelopes] Use `EmbedUpdateEnvelope` to embed envelope editing: ```jsx import { EmbedUpdateEnvelope } from '@documenso/embed-react'; const EnvelopeEditor = ({ presignToken, envelopeId }) => { return (
{ console.log('Envelope updated:', data.envelopeId); }} />
); }; ``` *** ## Props [#props] ### All V2 Editor Components [#all-v2-editor-components] | Prop | Type | Required | Description | | ------------------ | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `presignToken` | `string` | Yes | Authentication token from your backend | | `externalId` | `string` | No | Your reference ID to link with the envelope | | `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` | | `css` | `string` | No | Custom CSS string (Platform Plan) | | `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) | | `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) | | `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) | | `className` | `string` | No | CSS class for the iframe | | `user` | `object` | No | Current user info. When provided, enables the "Add Myself" button in the recipients list. Object with optional `email` and `name` fields | | `features` | `object` | No | Feature toggles for the editor experience | ### Create Component Only [#create-component-only] | Prop | Type | Required | Description | | ---------- | ---------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"DOCUMENT"` \| `"TEMPLATE"` | Yes | Whether to create a document or template envelope | | `folderId` | `string` | No | The ID of the folder to create the envelope in. If not provided, the envelope is created in the root folder. The folder must match the envelope type and team. | ### Update Component Only [#update-component-only] | Prop | Type | Required | Description | | ------------ | -------- | -------- | ----------------------- | | `envelopeId` | `string` | Yes | The envelope ID to edit | *** ## Feature Toggles [#feature-toggles] V2 editor provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the editor experience — any omitted fields will use their defaults. ```jsx ``` ### General [#general] Controls the overall editor flow and UI: | Property | Type | Default | Description | | ----------------------------- | --------- | ------- | ------------------------------------------------ | | `allowConfigureEnvelopeTitle` | `boolean` | `true` | Allow editing the envelope title | | `allowUploadAndRecipientStep` | `boolean` | `true` | Show the upload and recipient configuration step | | `allowAddFieldsStep` | `boolean` | `true` | Show the add fields step | | `allowPreviewStep` | `boolean` | `true` | Show the preview step | | `minimizeLeftSidebar` | `boolean` | `true` | Minimize the left sidebar by default | ### Settings [#settings] Controls envelope configuration options. Set to `null` to hide envelope settings entirely. | Property | Type | Default | Description | | -------------------------------- | --------- | ------- | --------------------------------------- | | `allowConfigureSignatureTypes` | `boolean` | `true` | Allow configuring signature types | | `allowConfigureLanguage` | `boolean` | `true` | Allow configuring the language | | `allowConfigureDateFormat` | `boolean` | `true` | Allow configuring the date format | | `allowConfigureTimezone` | `boolean` | `true` | Allow configuring the timezone | | `allowConfigureRedirectUrl` | `boolean` | `true` | Allow configuring a redirect URL | | `allowConfigureDistribution` | `boolean` | `true` | Allow configuring distribution settings | | `allowConfigureExpirationPeriod` | `boolean` | `true` | Allow configuring the expiration period | | `allowConfigureEmailSender` | `boolean` | `true` | Allow configuring the email sender | | `allowConfigureEmailReplyTo` | `boolean` | `true` | Allow configuring the email reply-to | ### Actions [#actions] Controls available actions during editing: | Property | Type | Default | Description | | ------------------ | --------- | ------- | ------------------------ | | `allowAttachments` | `boolean` | `true` | Allow adding attachments | ### Envelope Items [#envelope-items] Controls how envelope items (individual files within the envelope) can be managed. Set to `null` to prevent any item modifications. | Property | Type | Default | Description | | --------------------- | --------- | ------- | ----------------------------- | | `allowConfigureTitle` | `boolean` | `true` | Allow editing item titles | | `allowConfigureOrder` | `boolean` | `true` | Allow reordering items | | `allowUpload` | `boolean` | `true` | Allow uploading new items | | `allowDelete` | `boolean` | `true` | Allow deleting items | | `allowReplace` | `boolean` | `true` | Allow replacing an item's PDF | ### Recipients [#recipients] Controls recipient configuration options. Set to `null` to prevent any recipient modifications. | Property | Type | Default | Description | | --------------------------------- | --------- | ------- | ------------------------------------- | | `allowConfigureSigningOrder` | `boolean` | `true` | Allow configuring the signing order | | `allowConfigureDictateNextSigner` | `boolean` | `true` | Allow configuring dictate next signer | | `allowApproverRole` | `boolean` | `true` | Allow the approver recipient role | | `allowViewerRole` | `boolean` | `true` | Allow the viewer recipient role | | `allowCCerRole` | `boolean` | `true` | Allow the CC recipient role | | `allowAssistantRole` | `boolean` | `true` | Allow the assistant recipient role | ### Disabling Steps [#disabling-steps] You can also disable entire steps of the editor flow. This allows you to skip steps that are not relevant to your use case: ```jsx ``` *** ## Event Callbacks [#event-callbacks] ### `onEnvelopeCreated` [#onenvelopecreated] Fired when an envelope is successfully created: | Field | Type | Description | | ------------ | ---------------- | --------------------------------------- | | `envelopeId` | `string` | The ID of the created envelope | | `externalId` | `string \| null` | Your external reference ID, if provided | ### `onEnvelopeUpdated` [#onenvelopeupdated] Fired when an envelope is successfully updated: | Field | Type | Description | | ------------ | ---------------- | --------------------------------------- | | `envelopeId` | `string` | The ID of the updated envelope | | `externalId` | `string \| null` | Your external reference ID, if provided | *** ## Complete Integration Example [#complete-integration-example] This example shows a full workflow where users create an envelope and then edit it: ```tsx import { useState } from 'react'; import { EmbedCreateEnvelope, EmbedUpdateEnvelope } from '@documenso/embed-react'; const EnvelopeManager = ({ presignToken }) => { const [envelopeId, setEnvelopeId] = useState(null); const [mode, setMode] = useState('create'); if (mode === 'success') { return (

Envelope updated successfully

); } if (mode === 'edit' && envelopeId) { return (
{ console.log('Envelope updated:', data.envelopeId); setMode('success'); }} />
); } return (
{ console.log('Envelope created:', data.envelopeId); setEnvelopeId(data.envelopeId); setMode('edit'); }} />
); }; ``` *** ## See Also [#see-also] * [Editor Overview](/docs/developers/embedding/editor) - V1 vs V2 comparison and presign tokens * [V1 Editor](/docs/developers/embedding/editor/v1) - V1 document and template editor * [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance # Angular ## Installation [#installation] `npm install @documenso/embed-angular` `yarn add @documenso/embed-angular` `pnpm add @documenso/embed-angular` *** ## Direct Template [#direct-template] ```typescript import { Component } from '@angular/core'; import { EmbedDirectTemplate } from '@documenso/embed-angular'; @Component({ selector: 'app-signing', standalone: true, imports: [EmbedDirectTemplate], template: ` `, }) export class SigningComponent { token = 'your-template-token'; onReady() { console.log('Ready'); } onCompleted(data: { documentId: number }) { console.log('Signed:', data.documentId); } onError() { console.error('Error'); } } ``` *** ## Signing Token [#signing-token] ```typescript import { Component, Input } from '@angular/core'; import { EmbedSignDocument } from '@documenso/embed-angular'; @Component({ selector: 'app-signing', standalone: true, imports: [EmbedSignDocument], template: ` `, }) export class SigningComponent { @Input() token = ''; onCompleted(data: { documentId: number }) { console.log('Signed:', data.documentId); } } ``` *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Editor](/docs/developers/embedding/editor) - Embed document creation # SDKs Install the SDK for your framework and embed document signing with a few lines of code. If you are not using a JavaScript framework, you can embed signing using [Direct Links](/docs/developers/embedding/direct-links) with a plain iframe or redirect. # Preact ## Installation [#installation] `npm install @documenso/embed-preact` `yarn add @documenso/embed-preact` `pnpm add @documenso/embed-preact` *** ## Direct Template [#direct-template] ```tsx import { EmbedDirectTemplate } from '@documenso/embed-preact'; const SigningPage = () => { return ( console.log('Ready')} onDocumentCompleted={(data) => { console.log('Signed:', data.documentId); }} onDocumentError={() => console.error('Error')} /> ); }; ``` *** ## Signing Token [#signing-token] ```tsx import { EmbedSignDocument } from '@documenso/embed-preact'; const SigningPage = ({ token }: { token: string }) => { return ( { console.log('Signed:', data.documentId); }} /> ); }; ``` *** ## Styling (Platform Plan) [#styling-platform-plan] ```tsx import { EmbedDirectTemplate } from '@documenso/embed-preact'; const StyledEmbed = () => { return ( ); }; ``` See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables. *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Editor](/docs/developers/embedding/editor) - Embed document creation # React ## Installation [#installation] `npm install @documenso/embed-react` `yarn add @documenso/embed-react` `pnpm add @documenso/embed-react` *** ## Direct Template [#direct-template] ```tsx import { EmbedDirectTemplate } from '@documenso/embed-react'; const SigningPage = () => { return ( console.log('Ready')} onDocumentCompleted={(data) => { console.log('Signed:', data.documentId); }} onDocumentError={() => console.error('Error')} /> ); }; ``` *** ## Signing Token [#signing-token] ```tsx import { EmbedSignDocument } from '@documenso/embed-react'; const SigningPage = ({ token }: { token: string }) => { return ( { console.log('Signed:', data.documentId); }} /> ); }; ``` *** ## Styling (Platform Plan) [#styling-platform-plan] ```tsx import { EmbedDirectTemplate } from '@documenso/embed-react'; const StyledEmbed = () => { return ( ); }; ``` See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables. *** ## Complete Example [#complete-example] ```tsx import { useState } from 'react'; import { EmbedDirectTemplate } from '@documenso/embed-react'; type Status = 'loading' | 'ready' | 'completed' | 'error'; const DocumentSigning = ({ token }: { token: string }) => { const [status, setStatus] = useState('loading'); if (status === 'completed') { return

Thank you for signing the document.

; } if (status === 'error') { return

An error occurred. Please try again.

; } return (
{status === 'loading' && (
Loading...
)} setStatus('ready')} onDocumentCompleted={() => setStatus('completed')} onDocumentError={() => setStatus('error')} />
); }; ``` *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Editor](/docs/developers/embedding/editor) - Embed document creation # Solid ## Installation [#installation] `npm install @documenso/embed-solid` `yarn add @documenso/embed-solid` `pnpm add @documenso/embed-solid` *** ## Direct Template [#direct-template] ```tsx import { EmbedDirectTemplate } from '@documenso/embed-solid'; const SigningPage = () => { return ( console.log('Ready')} onDocumentCompleted={(data) => { console.log('Signed:', data.documentId); }} onDocumentError={() => console.error('Error')} /> ); }; ``` *** ## Signing Token [#signing-token] ```tsx import { EmbedSignDocument } from '@documenso/embed-solid'; const SigningPage = (props: { token: string }) => { return ( { console.log('Signed:', data.documentId); }} /> ); }; ``` *** ## Styling (Platform Plan) [#styling-platform-plan] ```tsx import { EmbedDirectTemplate } from '@documenso/embed-solid'; const StyledEmbed = () => { return ( ); }; ``` See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables. *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Editor](/docs/developers/embedding/editor) - Embed document creation # Svelte ## Installation [#installation] `npm install @documenso/embed-svelte` `yarn add @documenso/embed-svelte` `pnpm add @documenso/embed-svelte` *** ## Direct Template [#direct-template] ```svelte console.log('Ready')} onDocumentCompleted={onCompleted} onDocumentError={() => console.error('Error')} /> ``` *** ## Signing Token [#signing-token] ```svelte ``` *** ## Styling (Platform Plan) [#styling-platform-plan] ```svelte ``` See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables. *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Editor](/docs/developers/embedding/editor) - Embed document creation # Vue ## Installation [#installation] `npm install @documenso/embed-vue` `yarn add @documenso/embed-vue` `pnpm add @documenso/embed-vue` *** ## Direct Template [#direct-template] ```html ``` *** ## Signing Token [#signing-token] ```html ``` *** ## Styling (Platform Plan) [#styling-platform-plan] ```html ``` See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables. *** ## See Also [#see-also] * [Embedding Overview](/docs/developers/embedding) - Props reference and concepts * [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance * [Editor](/docs/developers/embedding/editor) - Embed document creation # AI Recipient & Field Detection ## Overview [#overview] This guide covers how to enable the AI recipient and field detection features when you self-host Documenso. ## What This Enables [#what-this-enables] * Detect recipients from uploaded PDFs (roles, names, emails when present). * Detect and place fields (signature, initials, name, email, date, text, number, radio, checkbox) onto draft envelopes. * Built-in rate limits (3 requests per minute per IP) to prevent abuse. ## Prerequisites [#prerequisites] * A Google Cloud project with the **Vertex AI API** enabled and billing active. * A **Vertex AI Express API key** with access to Gemini models (create via the [Vertex AI Express flow](https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview) and manage keys in [API keys](https://cloud.google.com/vertex-ai/generative-ai/docs/start/api-keys)). * Documenso version that includes the AI detection feature and the corresponding database migration. ## Configure Environment Variables [#configure-environment-variables] Add these variables to your deployment `.env` (or secret manager): ``` GOOGLE_VERTEX_PROJECT_ID="" GOOGLE_VERTEX_API_KEY="" # Optional, defaults to "global" GOOGLE_VERTEX_LOCATION="global" ``` Use a region close to your users if you need data residency considerations (e.g. `europe-west1`). If you omit the location, Documenso uses `global`. Not all models are available in every region; if a model is unavailable, switch to a supported region. ## Deploy with the Published Container [#deploy-with-the-published-container] * Use the official Documenso image (DockerHub or GHCR) and supply the Vertex env vars above. * Ensure migrations run on startup (the container runs `prisma migrate deploy` in production mode). * Restart the container after adding or changing Vertex env vars. ## Enable the Feature in Documenso [#enable-the-feature-in-documenso] Once the service is running with the Vertex env vars: {/* prettier-ignore */} ### Organisation settings [#organisation-settings] Go to **Settings** → **Document Preferences** → **AI Features** and set to **Enabled**. Teams that inherit organisation defaults will get this automatically. ### Team settings [#team-settings] If a team overrides organisation defaults: * Go to **Team Settings** → **Document Preferences** → **AI Features** * Choose **Enabled**, or **Inherit** to follow the organisation ### Verify in the editor [#verify-in-the-editor] Open a draft envelope and check: * **Recipients**: sparkle button for AI detection is visible * **Fields**: **Detect with AI** is available ## Troubleshooting [#troubleshooting] Wait a minute or two and retry (rate limit is 3/min per IP). Ensure the env vars are set, the server was restarted after setting them, and `aiFeaturesEnabled` is enabled at organisation/team level. Confirm the Vertex API key is valid and the project has Vertex AI enabled. Check server logs for status codes from Vertex. If issues persist, recheck env vars, restart the service, and confirm the Prisma migration was applied. *** ## See Also [#see-also] * [AI Detection (User Guide)](/docs/users/documents/advanced/ai-detection) - How to use AI detection features * [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options # Document Conversion ## Overview [#overview] Documenso can accept `.docx` uploads in addition to PDFs. When a user uploads a Word document, the Documenso server sends it to a [Gotenberg](https://gotenberg.dev) service which uses LibreOffice to convert it to PDF. The converted PDF is what gets stored, signed, and downloaded. The original DOCX is discarded. This feature is **opt-in for self-hosted instances**. When the conversion service is not configured, DOCX uploads are rejected in the UI and only PDFs are accepted. | Property | Value | | ----------------------- | ---------------------------------------------------------------- | | Conversion engine | [Gotenberg](https://gotenberg.dev) + LibreOffice | | Input format | `.docx` (Office Open XML Word documents) | | Output format | PDF | | Network requirement | Documenso must reach the Gotenberg HTTP API | | Default request timeout | 30 seconds per file | | Failure handling | An internal circuit breaker opens for 30 seconds after a failure | Only `.docx` is accepted. Legacy `.doc`, `.odt`, `.rtf`, and other LibreOffice-supported formats are rejected at the upload step even when Gotenberg is configured. *** ## Requirements [#requirements] * A running Gotenberg 8 instance with the LibreOffice module (`gotenberg/gotenberg:8-libreoffice` or newer). * Network reachability from the Documenso container to the Gotenberg HTTP API. * A version of Documenso that includes the document conversion feature. ## Build the Gotenberg Image [#build-the-gotenberg-image] The upstream `gotenberg/gotenberg:8-libreoffice` image works out of the box, but it ships only **metric-compatible font substitutes** (Carlito for Calibri, Liberation for Arial/Times/Courier). Layout widths are preserved but documents will look noticeably different from Word. For better fidelity, especially for non-Latin scripts, build a derived image that adds Microsoft Core Fonts and additional language fonts. The Documenso repository ships a reference Dockerfile at [`docker/development/Dockerfile.gotenberg`](https://github.com/documenso/documenso/blob/main/docker/development/Dockerfile.gotenberg) that you can use as a starting point: ```dockerfile FROM gotenberg/gotenberg:8-libreoffice USER root RUN echo "deb http://deb.debian.org/debian trixie contrib non-free" \ > /etc/apt/sources.list.d/contrib.list \ && echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" \ | debconf-set-selections \ && apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ ca-certificates \ ttf-mscorefonts-installer \ fonts-symbola \ fonts-noto-extra \ fonts-hosny-amiri \ fonts-thai-tlwg \ fonts-sil-padauk \ fonts-sarai \ fonts-samyak-taml \ culmus \ libfribidi0 \ libharfbuzz0b \ && fc-cache -f \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* USER gotenberg ``` `ttf-mscorefonts-installer` accepts the Microsoft Core Fonts EULA on your behalf via debconf. By installing this image you are agreeing to those licence terms. Review them before publishing the image. Build and publish the image to a registry you control: ```bash docker build -t registry.example.com/documenso/gotenberg:8 \ -f Dockerfile.gotenberg . docker push registry.example.com/documenso/gotenberg:8 ``` If you do not need extra fonts, skip the build step entirely and reference `gotenberg/gotenberg:8-libreoffice` directly in the next section. ## Deploy the Service [#deploy-the-service] The Gotenberg service should run **alongside** your Documenso container, not exposed to the public internet. The conversion service has no built-in authorisation beyond HTTP Basic auth, so it should sit on a private network or behind your existing reverse proxy. Add a `gotenberg` service to the `compose.yml` you use for Documenso: ```yaml services: gotenberg: image: registry.example.com/documenso/gotenberg:8 # Or use upstream directly: # image: gotenberg/gotenberg:8-libreoffice restart: unless-stopped environment: GOTENBERG_API_BASIC_AUTH_USERNAME: ${GOTENBERG_USERNAME} GOTENBERG_API_BASIC_AUTH_PASSWORD: ${GOTENBERG_PASSWORD} command: - gotenberg - --api-enable-basic-auth - --libreoffice-deny-private-ips - --api-timeout=500s - --libreoffice-auto-start - --libreoffice-start-timeout=300s - --pdfengines-disable-routes - --webhook-disable healthcheck: test: ['CMD', 'curl', '-fsS', 'http://localhost:3000/health'] interval: 10s timeout: 5s retries: 5 start_period: 20s documenso: # existing config environment: NEXT_PRIVATE_DOCUMENT_CONVERSION_URL: http://gotenberg:3000 NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME: ${GOTENBERG_USERNAME} NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD: ${GOTENBERG_PASSWORD} depends_on: gotenberg: condition: service_healthy ``` Do **not** publish Gotenberg's port (`3000`) to the host. Documenso reaches it over the internal Docker network using the service name (`http://gotenberg:3000`). Create a Deployment, Service, and Secret. Example manifests: ```yaml apiVersion: v1 kind: Secret metadata: name: gotenberg-auth namespace: documenso stringData: username: documenso password: replace-me-with-a-strong-password --- apiVersion: apps/v1 kind: Deployment metadata: name: gotenberg namespace: documenso spec: replicas: 1 selector: matchLabels: { app: gotenberg } template: metadata: labels: { app: gotenberg } spec: containers: - name: gotenberg image: registry.example.com/documenso/gotenberg:8 args: - gotenberg - --api-enable-basic-auth - --libreoffice-deny-private-ips - --api-timeout=500s - --libreoffice-auto-start - --libreoffice-start-timeout=300s - --pdfengines-disable-routes - --webhook-disable env: - name: GOTENBERG_API_BASIC_AUTH_USERNAME valueFrom: { secretKeyRef: { name: gotenberg-auth, key: username } } - name: GOTENBERG_API_BASIC_AUTH_PASSWORD valueFrom: { secretKeyRef: { name: gotenberg-auth, key: password } } ports: - containerPort: 3000 readinessProbe: httpGet: { path: /health, port: 3000 } livenessProbe: httpGet: { path: /health, port: 3000 } initialDelaySeconds: 30 --- apiVersion: v1 kind: Service metadata: name: gotenberg namespace: documenso spec: selector: { app: gotenberg } ports: - port: 3000 targetPort: 3000 ``` Then reference the in-cluster URL from Documenso's environment: ``` NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=http://gotenberg.documenso.svc.cluster.local:3000 NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password ``` Documenso does not have to colocate with Gotenberg. You can point it at any reachable Gotenberg deployment: a managed instance, a shared internal service, or a Gotenberg-compatible API. ```bash NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=https://gotenberg.internal.example.com NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password ``` The remote instance must: * Expose the LibreOffice route `/forms/libreoffice/convert`. * Be reachable from the Documenso container with low enough latency that the 30 second per-request timeout is comfortable. * Be on a private network or require authentication. Uploaded documents are sent to it as multipart form data and may contain sensitive content. ## Recommended Gotenberg Flags [#recommended-gotenberg-flags] The flags in the examples above are not arbitrary. Each one matters for a production deployment. | Flag | Why it matters | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--api-enable-basic-auth` | Requires HTTP Basic credentials on every API route. Without this, anyone with network access to the container can convert arbitrary documents. | | `--libreoffice-deny-private-ips` | Rejects any outbound fetch LibreOffice tries to make to private, loopback, link-local, or cloud-metadata addresses while processing a document. Mitigates SSRF via malicious `.docx` files that embed `TargetMode="External"` references. Requires Gotenberg 8.32.0. | | `--api-timeout=500s` | Server-side request ceiling. Documenso aborts at 30 s by default, so this is a safety net for very large documents. | | `--libreoffice-auto-start` | Starts LibreOffice at container boot so the first request is not slow. | | `--libreoffice-start-timeout=300s` | Allows LibreOffice up to 5 minutes to come up under load. | | `--pdfengines-disable-routes` | Disables the PDF engines routes Documenso does not use. Shrinks the attack surface. | | `--webhook-disable` | Disables webhook callbacks. Documenso uses synchronous requests only. | ## Configure Documenso [#configure-documenso] Set the following environment variables on the Documenso container and restart it. ### Required [#required] | Variable | Description | | -------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` | Base URL of the Gotenberg service (e.g., `http://gotenberg:3000`). Leave unset to disable the feature. | ### Optional [#optional] | Variable | Default | Description | | --------------------------------------------- | ------- | --------------------------------------------------------------------------------- | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME` | | HTTP Basic auth username. Set when Gotenberg runs with `--api-enable-basic-auth`. | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD` | | HTTP Basic auth password. Set together with the username. | | `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` | `30000` | Per-request timeout in milliseconds. Increase for very large documents. | When `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is set, the public flag `NEXT_PUBLIC_DOCUMENT_CONVERSION_ENABLED` is derived automatically on server start. You do not need to set it yourself, and setting it manually has no effect. ### Example `.env` Snippet [#example-env-snippet] ```bash # Document conversion (DOCX -> PDF) NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=http://gotenberg:3000 NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password # NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS=60000 ``` ## Verify the Setup [#verify-the-setup] {/* prettier-ignore */} ### Restart the Documenso container [#restart-the-documenso-container] Restart so the new environment variables are picked up. ### Confirm Gotenberg is healthy [#confirm-gotenberg-is-healthy] From a shell inside the Documenso container or another container on the same network: ```bash curl -fsS http://gotenberg:3000/health ``` The endpoint is exempt from basic auth and should return `200 OK`. ### Upload a test DOCX [#upload-a-test-docx] In the Documenso web UI, open **Documents** and try uploading a small `.docx` file. The upload dropzone should accept it, and after a few seconds the editor should open with the converted PDF. ### Check the server logs [#check-the-server-logs] Successful conversions log a `document_conversion_attempt` event with `result: "success"`, the duration, and the file size. Failures log the same event with `result: "error"` and an error code (`CONVERSION_SERVICE_UNAVAILABLE`, `CONVERSION_FAILED`, or `UNSUPPORTED_FILE_TYPE`). ## Security Considerations [#security-considerations] * **Treat the conversion service as untrusted internal infrastructure.** Documents pass through Gotenberg in plain form. Run it on a private network and require HTTP Basic auth. * **Run with `--libreoffice-deny-private-ips`.** Without this flag, a malicious `.docx` can trigger LibreOffice to fetch URLs from your internal network (SSRF). * **Disable unused routes.** `--pdfengines-disable-routes` and `--webhook-disable` reduce attack surface. Documenso only uses the LibreOffice convert route. * **Do not expose Gotenberg to the public internet.** Even with basic auth, this is a document-processing service with a non-trivial CPU and memory footprint; exposing it invites abuse. * **Rotate credentials.** Rotating the basic auth secret is a config change in both Gotenberg and Documenso, followed by a restart of each. ## Resource Sizing [#resource-sizing] Conversion is CPU- and memory-bound on LibreOffice. As a starting point: | Workload | Suggested resources | | ----------------------------- | ------------------------------------------------------------ | | Light (a few DOCX per minute) | 1 vCPU, 1 GB RAM | | Moderate (sustained uploads) | 2 vCPU, 2 GB RAM | | Heavy / multi-tenant | Horizontally scale Gotenberg replicas behind a load balancer | Gotenberg is stateless. Each container handles one or more concurrent requests independently. Scale horizontally rather than vertically once a single replica is saturated. ## Troubleshooting [#troubleshooting] The Documenso server does not see `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL`. Check the value is set on the running container (`docker exec documenso printenv | grep DOCUMENT_CONVERSION`) and restart after changing it. Documenso could not reach Gotenberg. Verify: * The URL in `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is resolvable from the Documenso container (use the Docker service name or in-cluster DNS, not `localhost`). * Gotenberg's `/health` endpoint returns `200`. * Basic auth credentials match between the two services. After repeated failures, an internal circuit breaker opens for 30 seconds. Subsequent uploads will fail fast during that window; this is intentional and self-recovers. Gotenberg was reachable but returned a non-2xx response. Check the Gotenberg container logs: ```bash docker compose logs -f gotenberg ``` Common causes: corrupted `.docx` file, exotic embedded objects LibreOffice cannot render, or a file that genuinely exceeded the conversion timeout. Increase `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` for very large documents. LibreOffice is not byte-identical to Microsoft Word. Layout, font metrics, and complex elements (Charts, SmartArt, ActiveX controls) may differ. To improve fidelity: * Use the custom Dockerfile in this guide to install Microsoft Core Fonts and additional language fonts. * Make sure any custom fonts referenced by your documents are installed in the Gotenberg image. * For pixel-perfect output, ask users to export to PDF from Word before uploading. Documenso disables Gotenberg's `exportFormFields` flag during conversion. Word content controls (``) become static graphics in the output PDF, which prevents Documenso's later flattening step from making them invisible. This is intentional. Use Documenso fields (signature, text, date, etc.) for anything that needs to be filled in by signers. LibreOffice starts lazily by default. Pass `--libreoffice-auto-start` to Gotenberg so it warms up at container boot. Allow up to a minute on first start before considering the service unhealthy. Repeated failures open an in-process circuit breaker for 30 seconds. If you see this in production, the underlying problem is the Gotenberg service. Check its logs, resource usage, and connectivity. The breaker is per-process and resets on restart. *** ## See Also [#see-also] * [Upload Documents (User Guide)](/docs/users/documents/upload) - End-user view of DOCX uploads * [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference * [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Compose-based deployment patterns * [Gotenberg Documentation](https://gotenberg.dev/docs/getting-started/introduction) - Upstream Gotenberg docs # Advanced # Setting up OAuth Providers ## Google OAuth (Gmail) [#google-oauth-gmail] To use Google OAuth, you will need to create a Google Cloud Platform project and enable the Google Identity and Access Management (IAM) API. You will also need to create a new OAuth client ID and download the client secret. ### Create and configure a new OAuth client ID [#create-and-configure-a-new-oauth-client-id] {/* prettier-ignore */} ### Open Google Cloud Console [#open-google-cloud-console] Go to the [Google Cloud Platform Console](https://console.cloud.google.com/) and select a project or create a new one from the projects list. ### Open APIs & services [#open-apis--services] From the console's left side menu, select **APIs & services** (if not already open). ### Open Credentials [#open-credentials] On the left, click **Credentials**. ### Create OAuth client ID [#create-oauth-client-id] Click **New Credentials**, then select **OAuth client ID**. When prompted, select **Web application**. ### Name and create the client [#name-and-create-the-client] Enter a name for your client ID and click **Create**. Click the download button to download the client secret. ### Set authorized origins and redirect URIs [#set-authorized-origins-and-redirect-uris] Set: * **Authorized JavaScript origins**: `https://` * **Authorized redirect URIs**: `https:///api/auth/callback/google` ### Set environment variables [#set-environment-variables] In your Documenso environment, set: ``` NEXT_PRIVATE_GOOGLE_CLIENT_ID= NEXT_PRIVATE_GOOGLE_CLIENT_SECRET= ``` Finally verify sign-in with Google by signing in with your Google account and checking the email address in your profile. ## Microsoft OAuth (Azure AD) [#microsoft-oauth-azure-ad] To use Microsoft OAuth, you will need to create an Azure AD application registration in the Microsoft Azure portal. This will allow users to sign in with their Microsoft accounts. ### Create and configure a new Azure AD application [#create-and-configure-a-new-azure-ad-application] {/* prettier-ignore */} ### Open Azure Portal [#open-azure-portal] Go to the [Azure Portal](https://portal.azure.com/) and navigate to **Azure Active Directory** (or **Microsoft Entra ID** in newer portals). ### Start app registration [#start-app-registration] In the left sidebar, click **App registrations**, then **New registration**. ### Name and choose account types [#name-and-choose-account-types] Enter a name (e.g. "Documenso"). Under **Supported account types**, select both: * Accounts in any organizational directory (Any Azure AD directory - Multitenant) * Personal Microsoft accounts (e.g. Skype, Xbox) This allows any Microsoft account to sign in. ### Set redirect URI and register [#set-redirect-uri-and-register] Under **Redirect URI**, select **Web** and enter: ```bash https:///api/auth/callback/microsoft ``` Click **Register**. ### Configure the application [#configure-the-application] {/* prettier-ignore */} ### Copy the Application (client) ID [#copy-the-application-client-id] After registration you're on the app overview page. Copy the **Application (client) ID** for `NEXT_PRIVATE_MICROSOFT_CLIENT_ID`. ### Create a client secret [#create-a-client-secret] * In the left sidebar, click **Certificates & secrets** * Under **Client secrets**, click **New client secret** * Add a description and select an expiration period, then click **Add** * Copy the **Value** (not the Secret ID): this is your `NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET` ### Set environment variables [#set-environment-variables-1] In your Documenso environment, set: ``` NEXT_PRIVATE_MICROSOFT_CLIENT_ID= NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET= ``` *** ## See Also [#see-also] * [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options * [Single Sign-On](/docs/users/organisations/single-sign-on) - Configure SSO for your organisation * [Microsoft Entra ID](/docs/users/organisations/single-sign-on/microsoft-entra-id) - Detailed Entra ID setup guide # CSC (AES / QES) The `csc` signing transport routes signatures through a third-party Trust Service Provider (TSP) using the [Cloud Signature Consortium API v1.0.4.0](https://cloudsignatureconsortium.org/). Each recipient authenticates directly with the TSP (Strong Customer Authentication) and the TSP returns a per-recipient signature bound to the document hash. Documenso assembles the resulting PAdES signature inside the PDF. This transport enables **Advanced Electronic Signatures (AES)** and **Qualified Electronic Signatures (QES)** under eIDAS. See [Signature Levels](/docs/compliance/signature-levels) for the legal framework. CSC mode is **instance-wide**: one CSC provider per Documenso install. All envelopes created while the instance runs in `csc` mode use AES or QES. Switching `NEXT_PRIVATE_SIGNING_TRANSPORT` is a one-way operational migration — see [Switching Transports](#switching-transports). CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. The instance refuses to start in `csc` mode without it. ## Prerequisites [#prerequisites] {/* prettier-ignore */} ### A TSP account [#a-tsp-account] Establish a relationship with a CSC-compatible Trust Service Provider. The TSP issues qualified or advanced certificates to your signers, holds the private keys in its HSM, and exposes a CSC v1.0.4.0-compliant API. ### OAuth client credentials [#oauth-client-credentials] Register Documenso as an OAuth client with the TSP. You will receive a client ID and client secret, and must supply Documenso's callback URL when registering: ``` ${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback ``` The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL` and the route mount path. There is no env var to override it; ensuring the registered URL matches your instance's webapp URL exactly is the operator's responsibility. ### Enterprise Edition license [#enterprise-edition-license] CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one. ### S3 storage (strongly recommended) [#s3-storage-strongly-recommended] CSC produces multiple `DocumentData` rows per envelope item (one per recipient signature, plus the materialised and source rows). Database-backed storage base64-inflates each row by \~33% and is impractical at meaningful PDF sizes. Configure [S3 storage](/docs/self-hosting/configuration/storage) before enabling CSC. ## Environment Variables [#environment-variables] | Variable | Description | Default | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `csc` | | | `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | | | `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | | | `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | | | `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller does not specify one. `AES` or `QES`. Explicit requests always pass through. | `AES` | | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | **Required.** Comma-separated RFC 3161 TSA URLs. Always used for B-LTA archival timestamps at seal time, and also serves as the B-T sign-time fallback when the TSP does not expose `signatures/timestamp`. The instance refuses to start in CSC mode without it. See [Timestamp Authority Resolution](#timestamp-authority-resolution). | | `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` is set automatically from `NEXT_PRIVATE_SIGNING_TRANSPORT` at server startup. Do not set it manually — see [Environment Variables](/docs/self-hosting/configuration/environment#derived-public-variables). ## Configuration Example [#configuration-example] ```bash NEXT_PRIVATE_SIGNING_TRANSPORT=csc NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=https://api.example-tsp.com/csc/v1 NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=documenso-prod NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=... NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=QES NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=http://timestamp.example.com ``` Register `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` (e.g. `https://sign.example.com/api/csc/oauth/callback`) as the OAuth callback URL with the TSP. ## Default Signature Level [#default-signature-level] `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` selects the legal tier applied to envelopes that do not specify one explicitly. It is a default, not a capability gate: callers may still create AES or QES envelopes explicitly regardless of this setting. | Configured value | Caller passes nothing | Caller passes `AES` | Caller passes `QES` | | ---------------- | --------------------- | ------------------- | ------------------- | | `AES` (default) | Envelope is `AES` | Envelope is `AES` | Envelope is `QES` | | `QES` | Envelope is `QES` | Envelope is `AES` | Envelope is `QES` | Any value other than `AES` or `QES` causes the instance to refuse to start. This prevents silent qualified-to-advanced downgrades from a typo. ## Timestamp Authority Resolution [#timestamp-authority-resolution] AES/QES envelopes use TSA-attested timestamps in two distinct phases. Resolution differs per phase. ### Sign time — PAdES B-T per recipient [#sign-time--pades-b-t-per-recipient] Each recipient's CMS embeds a signature timestamp (CMS unsigned attribute) so proven time is bound to the recipient's signature itself. Resolution order: 1. If the TSP advertises `signatures/timestamp` in its `info` response (CSC §11.10), the TSP endpoint is used. The call is authorised with **this recipient's** service-scope bearer token — the same one authorising the `signatures/signHash` call alongside it. 2. Otherwise, the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is used (RFC 3161 over HTTP). Selection is made at boot from the discovered transport, not at runtime; there is no try-then-fall-through. If the chosen source fails, the recipient's sign attempt fails. ### Seal time — PAdES B-LTA archival [#seal-time--pades-b-lta-archival] The seal-document job emits a single archival `/DocTimeStamp` over the fully-signed envelope (plus DSS for the existing signatures and the timestamp's own chain). This phase is **env-only**: the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is always used. The archival anchor is the operator's long-term trust anchor and SHOULD point at a dedicated qualified archival TSA (e.g. DigiCert) independent of the per-recipient TSP. We deliberately do not fall back to the TSP at seal time: archive longevity should not be coupled to a TSP that may rotate or revoke, and the seal-document job has no recipient context to carry a service-scope bearer. ### Boot-time guard [#boot-time-guard] The instance refuses to start in CSC mode unless `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is set (`CSC_PROVIDER_NO_TSA` at transport construction). The env var is required unconditionally — even when the TSP advertises its own `signatures/timestamp`, seal-time B-LTA archival uses the env TSA. Catching this at boot prevents the failure mode where an envelope signs successfully at B-T and then hangs in `WAITING_FOR_SIGNATURE_COMPLETION` when the seal job throws. ## Switching Transports [#switching-transports] `NEXT_PRIVATE_SIGNING_TRANSPORT` is a one-way operational migration. Existing envelopes route per the `signatureLevel` column they were created with — the runtime branching looks at the envelope, not the env var. After a switch: * Envelopes already at `SES` continue to use the new transport for sealing, but the new transport's signer must produce SES-compatible signatures (only `local` and `gcloud-hsm` qualify). * Envelopes already at `AES` / `QES` will fail at sign or seal time if the new transport is not `csc`. Plan migrations during a quiet window with no in-flight envelopes. ## Behavioural Notes [#behavioural-notes] CSC mode changes a number of envelope-authoring behaviours that operators should communicate to users. ### Mutation lock at distribution [#mutation-lock-at-distribution] For AES/QES envelopes, all authoring routes refuse mutations once the envelope leaves DRAFT. This locks the PDF before any recipient begins Strong Customer Authentication, closing the PDF-swap window that would otherwise allow an owner to replace the PDF between view and sign and break the legal "what you see is what you sign" guarantee. In practice: edit envelope, recipients, fields, and items freely while DRAFT; once sent, no changes are accepted (including from the API). ### Sequential signing only [#sequential-signing-only] Parallel signing produces conflicting incremental updates over the same base PDF, breaking the per-recipient `/ByteRange` invariant. The signing order is forced to `SEQUENTIAL` on AES/QES envelopes — at the schema layer, at send time, and in the UI (the parallel-signing toggle is hidden). ### Assistant role and Dictate Next Signer disabled [#assistant-role-and-dictate-next-signer-disabled] Both features modify the recipient set after the envelope is sent, which is incompatible with the AES/QES mutation lock. They are hidden in the UI and rejected at the server schema layer. ### Sidecar PDFs at download [#sidecar-pdfs-at-download] The signed PDF must remain byte-identical to what each recipient's TSP signature authorised — Documenso cannot decorate it after signing. Audit logs and the Certificate of Completion are generated on demand and delivered as separate PDFs: * `GET /sign/{token}/download` returns the signed PDF only (or a ZIP for multi-item envelopes). * `GET /sign/{token}/download?version=bundle` returns a ZIP containing the signed PDFs, audit log PDF, and Certificate of Completion. * The completion email attaches all three. ## Recipient Flow [#recipient-flow] For context when supporting end users, here is what a recipient experiences on an AES/QES envelope: 1. Opens the email link, lands on the signing page. 2. Documenso redirects to the TSP for Strong Customer Authentication (first visit only; cached for the session lifetime). 3. Fills fields as normal. 4. Clicks Sign → redirected to the TSP for a second authentication round (issues a per-document Signature Activation Data token). 5. Returns to Documenso; the signing call completes within \~15 seconds. 6. Sees the standard completion screen. If the TSP returns no eligible credentials for the recipient (e.g. they have not enrolled), they see a blocking page directing them to enrol with the TSP and retry. ## Error Codes [#error-codes] CSC-specific error codes surfaced through the standard error channels: | Code | Meaning | Recovery | | ---------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `CSC_UNLICENSED` | License flag absent at transport-create | Operator: enable Enterprise Edition, restart | | `CSC_PROVIDER_INFO_FAILED` | `info` discovery failed at startup | Operator: check TSP availability and `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | | `CSC_PROVIDER_NO_TSA` | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is unset | Operator: configure `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | | `CSC_CREDENTIAL_LIST_EMPTY` | TSP returned no credentials for the user | Recipient: enrol with the TSP | | `CSC_CERT_INVALID` | Certificate refused at credential validation | Recipient: contact the TSP | | `CSC_ALGORITHM_REFUSED` | Signature algorithm fails policy | Operator/recipient: TSP does not meet policy (see below) | | `CSC_SAD_EXPIRED_PRE_SIGN` | Signature Activation Data expired before signing | Recipient: retry from Sign | | `CSC_TSP_TIMEOUT` | 15-second synchronous timeout reached | Recipient: retry (idempotent — the TSP enforces single-use SAD binding) | | `CSC_EMBED_FAILED` | Sign-time digest diverged from prep capture | Recipient: retry from Sign | | `CSC_BASE_DOCUMENT_MUTATED` | Document data changed between prep and sign | Operator: investigate (structural guard violation) | | `CSC_INSTANCE_MODE_MISMATCH` | Envelope created with wrong level for transport | Caller: use a level matching the instance transport | | `CSC_REQUEST_FAILED` | TSP HTTP transport failure — network error, non-2xx, or malformed response | Operator: check TSP availability; carries the TSP HTTP status and error in the message | ## Algorithm Policy [#algorithm-policy] Documenso refuses TSP credentials that do not meet the following minimums, at the OAuth callback boundary and again at sign time: | Class | Allowed | Refused | | ----- | ------------------------- | ----------------------------------------------- | | RSA | `key.len >= 2048` | Missing `key.len`, `key.len < 2048` | | ECDSA | P-256, P-384, P-521 | Missing `key.curve`, P-192, P-224, other curves | | Hash | SHA-256, SHA-384, SHA-512 | SHA-1, MD5 | | Other | — | DSA | This is the union of CSC v1.0.4.0 §11.5 requirements and current cryptographic guidance. ## Related [#related] * [Signature Levels](/docs/compliance/signature-levels) — AES / QES legal framework * [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) — overview of all signing transports * [Environment Variables](/docs/self-hosting/configuration/environment) — full env reference * [Enterprise Edition](/docs/policies/enterprise-edition) — license requirements # Google Cloud HSM Google Cloud HSM provides hardware-based key protection. The private key never leaves the HSM. ## Prerequisites [#prerequisites] {/* prettier-ignore */} ### Enable Cloud KMS [#enable-cloud-kms] Create or use a Google Cloud project and enable the Cloud KMS API. ### Create HSM key [#create-hsm-key] Create an HSM key ring and an asymmetric signing key in Cloud KMS (see [Creating an HSM Key](#creating-an-hsm-key) below). ### Create service account [#create-service-account] Create a service account with the `cloudkms.signerVerifier` role so Documenso can use the key. ### Export public certificate [#export-public-certificate] Export the public certificate from the HSM key; Documenso needs it to embed in signatures. ## Environment Variables [#environment-variables] | Variable | Description | | -------------------------------------------------------------- | ---------------------------------------------------- | | `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `gcloud-hsm` | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH` | Full resource path to the HSM key version | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH` | Path to the public certificate file | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS` | Base64-encoded public certificate | | `NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS` | Base64-encoded Google Cloud service account JSON | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH` | Path to the certificate chain file | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval | ## Configuration Example [#configuration-example] ```bash NEXT_PRIVATE_SIGNING_TRANSPORT=gcloud-hsm NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH=projects/my-project/locations/global/keyRings/documenso/cryptoKeys/signing-key/cryptoKeyVersions/1 NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH=/opt/documenso/public.crt NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS=eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii... ``` ## Key Path Format [#key-path-format] The HSM key path follows this format: ``` projects/{project}/locations/{location}/keyRings/{keyring}/cryptoKeys/{key}/cryptoKeyVersions/{version} ``` Example: ``` projects/my-company/locations/us-east1/keyRings/document-signing/cryptoKeys/prod-signing/cryptoKeyVersions/1 ``` ## Creating an HSM Key [#creating-an-hsm-key] Using gcloud CLI: ```bash # Create a key ring gcloud kms keyrings create document-signing \ --location=us-east1 \ --project=my-project # Create an asymmetric signing key gcloud kms keys create prod-signing \ --keyring=document-signing \ --location=us-east1 \ --purpose=asymmetric-signing \ --default-algorithm=rsa-sign-pkcs1-2048-sha256 \ --protection-level=hsm \ --project=my-project ``` # Signing Certificate When a document is completed in Documenso, it is digitally signed using an X.509 certificate. This cryptographic signature provides: * Proof of document authenticity * Verification that the document has not been modified since signing * Identity information about the signing entity Self-hosted Documenso instances require a signing certificate. You can generate a self-signed certificate or purchase one from a Certificate Authority (CA). ## Certificate Options [#certificate-options] A self-signed certificate is sufficient for most use cases where your industry has no special signing regulations. **Advantages:** * Free to create * Full control over certificate details * Works for internal and business documents **Limitations:** * PDF readers like Adobe Acrobat will not show a green checkmark * Not recognized by Adobe's trust list * Recipients see a warning that the signature cannot be verified The certificate still includes your organisation details and guarantees document integrity. Purchase a certificate from a Certificate Authority if you need: * Green checkmark in Adobe PDF readers * Industry-specific compliance requirements * Third-party signature validation For Adobe recognition, choose a vendor from the [Adobe Approved Trust List](https://helpx.adobe.com/acrobat/kb/approved-trust-list1.html). For organisations requiring hardware-based key protection, Documenso supports Google Cloud HSM. This provides: * FIPS 140-2 Level 3 certified key storage * Keys never leave the HSM * Audit logging of all cryptographic operations See [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) for setup instructions. For Advanced and Qualified Electronic Signatures under eIDAS, Documenso integrates with third-party Trust Service Providers via the Cloud Signature Consortium API. Each recipient authenticates directly with the TSP, which holds the private key and issues the signature. * Per-recipient identity verification by an accredited TSP * Legally equivalent to a handwritten signature within the EU (QES) * Requires an [Enterprise Edition](/docs/policies/enterprise-edition) license * Instance-wide setting; one CSC provider per Documenso install See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for setup instructions. # Local Certificate ## Generating a Self-Signed Certificate [#generating-a-self-signed-certificate] Create a self-signed certificate using OpenSSL. These commands work on Linux, macOS, and Windows Subsystem for Linux (WSL). {/* prettier-ignore */} ### Generate a private key [#generate-a-private-key] Create a 2048-bit RSA private key: ```bash openssl genrsa -out private.key 2048 ``` ### Create a self-signed certificate [#create-a-self-signed-certificate] Generate a certificate valid for one year: ```bash openssl req -new -x509 -key private.key -out certificate.crt -days 365 ``` When prompted, enter your organisation details: | Field | Example | | ------------------- | --------------------------------------------- | | Country Name | US | | State or Province | California | | Locality Name | San Francisco | | Organization Name | Your Company Inc | | Organizational Unit | Engineering | | Common Name | Your Company Signing CA | | Email Address | [admin@example.com](mailto:admin@example.com) | ### Create the .p12 certificate [#create-the-p12-certificate] Combine the private key and certificate into a PKCS#12 (.p12) file: ```bash openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt ``` You must set a password when prompted. Certificates without passwords cause signing failures with the error "Failed to get private key bags". To set the password non-interactively: ```bash # Set password securely (won't appear in command history) read -s -p "Enter certificate password: " CERT_PASS echo openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \ -password env:CERT_PASS ``` ### Clean up [#clean-up] Remove the intermediate files: ```bash rm private.key certificate.crt ``` Keep `certificate.p12` and the password secure. ## Using an Existing Certificate [#using-an-existing-certificate] If you have an existing certificate from a CA, convert it to PKCS#12 format. If you have separate certificate and key files: ```bash openssl pkcs12 -export -out certificate.p12 \ -inkey private.key \ -in certificate.crt \ -certfile chain.crt ``` Include `-certfile chain.crt` if you have intermediate certificates. **Convert from DER to PEM first:** ```bash openssl x509 -inform DER -in certificate.der -out certificate.crt openssl rsa -inform DER -in private.der -out private.key ``` **PFX files** are the same format as P12. You can use them directly by renaming to `.p12`. ## Certificate Validation [#certificate-validation] Verify your certificate is valid: ```bash # Check certificate details openssl pkcs12 -in certificate.p12 -info -nokeys # Verify password works openssl pkcs12 -in certificate.p12 -noout ``` ## Environment Variables [#environment-variables] | Variable | Description | | ------------------------------------------ | -------------------------------------------------------------- | | `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `local` (default) | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to the `.p12` certificate file | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` file contents (alternative to file path) | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the certificate | ## Using File Path [#using-file-path] Mount the certificate file and set the path: ```bash NEXT_PRIVATE_SIGNING_TRANSPORT=local NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password ``` **Docker example:** ```bash docker run -d \ -v /path/to/certificate.p12:/opt/documenso/cert.p12:ro \ -e NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 \ -e NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" \ documenso/documenso:latest ``` ## Using Base64-Encoded Contents [#using-base64-encoded-contents] For environments where file mounting is not available (e.g., Railway, Vercel): ```bash # Encode the certificate base64 -i certificate.p12 ``` Set the encoded string as an environment variable: ```bash NEXT_PRIVATE_SIGNING_TRANSPORT=local NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS=MIIKEQIBAzCCCdcGCSqGSIb3DQEHAaCCCcg... NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password ``` The `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` variable takes precedence over `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` if both are set. ## Docker File Permissions [#docker-file-permissions] When running in Docker, the application runs as user 1001. The certificate file must be readable: ```bash # On the host, before mounting sudo chown 1001 certificate.p12 chmod 400 certificate.p12 ``` Or mount as read-only and ensure group readability: ```bash chmod 440 certificate.p12 ``` # Timestamp Server Timestamp servers add a trusted timestamp to signatures, proving when the document was signed. This enables Long-Term Validation (LTV) and archival timestamps. ## Environment Variable [#environment-variable] | Variable | Description | | ------------------------------------------ | ------------------------------------------------ | | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated list of timestamp authority URLs | ## Configuration [#configuration] ```bash NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=http://timestamp.digicert.com,http://timestamp.globalsign.com/tsa/r6advanced1 ``` ## Free Timestamp Servers [#free-timestamp-servers] | Provider | URL | | ---------- | ------------------------------------------------- | | DigiCert | `http://timestamp.digicert.com` | | GlobalSign | `http://timestamp.globalsign.com/tsa/r6advanced1` | | FreeTSA | `https://freetsa.org/tsr` | | Sectigo | `http://timestamp.sectigo.com` | Timestamp servers add latency to the signing process. Each server in the list is tried in order until one succeeds. ## Benefits of Timestamping [#benefits-of-timestamping] * Proves when the document was signed * Signature remains valid after certificate expiration * Required for some compliance standards * Enables long-term archival ## Signature Appearance [#signature-appearance] ### Contact Information [#contact-information] Set contact information embedded in PDF signatures: ```bash NEXT_PUBLIC_SIGNING_CONTACT_INFO=support@example.com ``` If not set, defaults to your `NEXT_PUBLIC_WEBAPP_URL`. ### Legacy Signature Format [#legacy-signature-format] For compatibility with older PDF readers, use the legacy signature subfilter: ```bash NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER=true ``` | Subfilter | Description | Default | | --------------------- | ----------------------------- | ------- | | `ETSI.CAdES.detached` | Modern CAdES-based signatures | Yes | | `adbe.pkcs7.detached` | Legacy PKCS#7 signatures | No | Use the legacy format only if recipients report compatibility issues with older software. # Troubleshooting Cause: * Certificate has no password set Solution: * Verify the certificate has a password set * Re-create with a password: ```bash openssl pkcs12 -export -out new-certificate.p12 -inkey private.key -in certificate.crt ``` Cause: * Certificate path or contents not configured Solution: * Verify that `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` or `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` is set * Check the file exists at the configured path * Verify file permissions allow reading Cause: * Application cannot read the certificate file Solution: * For Docker, run `sudo chown 1001 certificate.p12` and `chmod 400 certificate.p12` on the host before mounting Cause: * Incorrect passphrase Solution: * Verify `NEXT_PRIVATE_SIGNING_PASSPHRASE` matches the certificate password * Check for trailing whitespace * Test with `openssl pkcs12 -in certificate.p12 -noout` Cause: * Certificate validity period has ended Solution: * Generate a new certificate or obtain a renewed one * Update the certificate file and restart Documenso * Check expiration: ```bash openssl pkcs12 -in certificate.p12 -nokeys | openssl x509 -noout -dates ``` Cause: * Incorrect base64 encoding or line breaks Solution: * Encode without line breaks: ```bash # macOS base64 -i certificate.p12 | tr -d '\n' # Linux base64 -w 0 certificate.p12 ``` * Ensure the entire base64 string is on one line Cause: * Google Cloud credentials not configured or invalid Solution: * Verify that `NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS` is set * Check the service account has `cloudkms.signerVerifier` role * Verify the base64 encoding of the service account JSON # AI Recipient & Field Detection ## Overview [#overview] Documenso can suggest recipients and place fields automatically using Google Vertex AI (Gemini). The feature is optional and only available when your organisation or team has **AI Features** enabled. Documents are processed securely and providers do not retain your data for training. ## Requirements [#requirements] * AI Features must be enabled in **Document Preferences** for your organisation or team. * The envelope must be in **Draft** status. * Helpful rate limits are in place (up to 3 detection requests per minute per IP) to prevent abuse. If you see a "too many requests" message, wait a minute or two and try again. ### Enable AI features [#enable-ai-features] {/* prettier-ignore */} Go to **Settings** > **Document Preferences** > **AI Features**. Set to **Enabled**. *This applies to teams that inherit organisation defaults.* {/* prettier-ignore */} Go to **Team Settings** > **Document Preferences** > **AI Features**. Choose **Enabled**, **Disabled**, or **Inherit**. ## Detect Recipients [#detect-recipients] Use this to identify who needs to sign or approve. {/* prettier-ignore */} Open a draft document or template and go to the **Recipients** panel. Select the **sparkle** button to start detection. If AI is enabled, uploads from the dashboard may open the detector automatically. Detect recipients with AI button in the Recipients panel Wait for progress to finish, then review the suggested recipients. Remove any incorrect entries, then click **Add recipients** to apply. Existing recipients and duplicates are preserved. Detection is unavailable once an envelope is completed. You can re-run detection if you update the document; each run counts toward the rate limit. ## Detect Fields [#detect-fields] Use this to auto-place fields on the pages of a draft. {/* prettier-ignore */} Open the envelope editor and switch to the **Fields** tab. Select **Detect with AI**. Optionally provide context (e.g., "Alice is the tenant, Bob is the landlord") to improve recipient assignment. AI field detection button AI field detection dialog with context input Watch the progress indicators (per page and total fields found). Review the summary and choose **Add fields** to place them in the editor. Works only for draft envelopes and teams with AI features enabled. Existing fields are masked during detection to avoid duplicates. Fields are assigned to recipients based on nearby labels and your context message; you can edit them after adding. ## Best Practices [#best-practices] Place labels close to the intended fields (e.g. "Tenant signature", "Buyer email") to improve detection accuracy. Add brief context when recipient roles are unclear to help the AI assign fields correctly. AI assists but does not replace final checks. Review all suggestions before sending documents. ## See Also [#see-also] * [Add Recipients](/docs/users/documents/add-recipients) - Manually add recipients to documents * [Add Fields](/docs/users/documents/add-fields) - Manually place fields on documents * [AI Features (Self-Hosting)](/docs/self-hosting/configuration/advanced/ai-features) - Configure AI detection for self-hosted instances # Default Document Recipients ## Overview [#overview] Documenso allows you to set default recipients for your documents. This is useful when you require specific recipients to be added to every document you send. You can add default recipients with the same roles as the recipients you can add when sending a document: The recipient will be required to sign the document. The recipient will be required to approve the document. The recipient will be required to view the document. The recipient will receive a copy of the document. You can set default recipients at the organisation or team level. ### Organisation level [#organisation-level] To set default recipients at the organisation level, navigate to the organisation settings page and click the **"Document"** tab under the **"Preferences"** section. Then scroll down to the **"Default Recipients"** section and add the recipients you want to be included in every document you send. A screenshot of the organisation's default recipients page The recipients are added with the **"CC"** role by default, but you can select a different role for each recipient. A screenshot of the organisation's default recipients page when selecting the role of the recipient ### Team level [#team-level] Setting the default recipients at the team level follows the same process as setting them at the organisation level. Setting the default recipients at the team level will override organisation-level defaults. To set default recipients at the team level, navigate to the team settings page and click the **"Document"** tab under the **"Preferences"** section. Then scroll down to the **"Default Recipients"** section. By default, the team will inherit the default recipients from the organisation. You can override these defaults by adding the recipients you want to be added to every document you send. A screenshot of the team's default recipients page ## See Also [#see-also] * [Add Recipients](/docs/users/documents/add-recipients) - Add recipients when sending a document * [Document Preferences](/docs/users/organisations/preferences/document) - Configure other document defaults * [Recipient Roles](/docs/concepts/recipient-roles) - Learn about the different recipient roles # Document Visibility ## Overview [#overview] The default document visibility option allows you to control who can view and access the documents uploaded within a team. This value can either be set in the [document preferences](/docs/users/organisations/preferences/document), or when you [create the document](/docs/users/documents/send) ## Document Visibility Options [#document-visibility-options] In **Default Document Visibility** (under document preferences), you can choose: All team members can access and view the document. Only managers and admins can access and view the document. Only admins can access and view the document. Use the organisation's default. Available for team-level settings only. The default is Everyone. Change it in document preferences. Document visibility preference ## How It Works [#how-it-works] When you create a document, its visibility depends on your role and the team's default. Select your role: New documents use the team's default visibility. You cannot change the visibility in the document editor. **If the default is "Everyone" or "Managers and above":** New documents use that setting. You can change it in the editor to "Everyone" or "Managers and above" (not "Admins only"). **If the default is "Admins only":** New documents are admins-only. You cannot change it. New documents use the team's default. You can change the visibility to any option in the document editor. You can change visibility at any time by editing the document and choosing a different option. A screenshot of the Documenso's document editor page where you can update the document visibility Updating the default document visibility in the team's general preferences will not affect the visibility of existing documents. You will need to update the visibility of each document individually. ## A Note on Document Access [#a-note-on-document-access] The `document owner` (the user who created the document) always has access to the document, regardless of the document's visibility settings. This means that even if a document is set to "Admins only", the document owner can still view and edit the document. The `recipient` (the user who receives the document for signature, approval, etc.) also has access to the document, regardless of the document's visibility settings. This means that even if a document is set to "Admins only", the recipient can still view and sign the document. ## See Also [#see-also] * [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings * [Team Members](/docs/users/organisations/members) - Understand team member roles * [Send Documents](/docs/users/documents/send) - Send documents for signing # Advanced # PDF Placeholders ## Overview [#overview] Documenso can automatically detect placeholder text in your PDF documents and create fields at those locations. This allows you to prepare documents in your preferred editing tool (Word, Google Docs, etc.) with placeholders that become signature fields when uploaded. ## How It Works [#how-it-works] When you upload a PDF, Documenso scans for text matching the placeholder pattern `{{...}}`. Each placeholder can specify: 1. **Field type**: What kind of field to create (signature, name, email, etc.) 2. **Recipient**: Which signer the field belongs to (r1, r2, etc.) 3. **Options**: Additional settings like required, read-only, font size, etc. The placeholder text is automatically hidden after fields are created, so your final document looks clean. ## Placeholder Format [#placeholder-format] The basic format is: ``` {{FIELD_TYPE, RECIPIENT, option1=value1, option2=value2}} ``` ### Examples [#examples] | Placeholder | Description | | ----------------------------- | ----------------------------------- | | `{{signature, r1}}` | Signature field for recipient 1 | | `{{name, r1}}` | Name field for recipient 1 | | `{{email, r2}}` | Email field for recipient 2 | | `{{date, r1}}` | Date field for recipient 1 | | `{{text, r1, required=true}}` | Required text field for recipient 1 | | `{{initials, r1}}` | Initials field for recipient 1 | ## Supported Field Types [#supported-field-types] The following field types are supported in placeholders: | Field Type | Placeholder Value | | ---------- | ----------------- | | Signature | `signature` | | Initials | `initials` | | Name | `name` | | Email | `email` | | Date | `date` | | Text | `text` | | Number | `number` | | Radio | `radio` | | Checkbox | `checkbox` | | Dropdown | `dropdown` | Field types are case-insensitive. `{{ SIGNATURE, r1 }}` and `{{ signature, r1 }}` are equivalent. ## Recipient Identifiers [#recipient-identifiers] Recipients are identified using `r1`, `r2`, `r3`, etc. The number corresponds to the order in which recipients are created: * `r1` - First recipient * `r2` - Second recipient * `r3` - Third recipient When you upload a PDF with placeholders, Documenso will: 1. Create placeholder recipients for each unique identifier found (e.g., `r1`, `r2`) 2. You can then update these with real email addresses before sending Placeholders without a recipient identifier (e.g., `{{ signature }}` without `r1`) are reserved for API use and will not create fields during upload. ## Field Options [#field-options] You can customize fields by adding options after the recipient identifier: ### Common Options [#common-options] | Option | Values | Description | | ----------- | ------------------------- | ------------------------------------------ | | `required` | `true`, `false` | Whether the field must be filled | | `readOnly` | `true`, `false` | Whether the field is pre-filled and locked | | `fontSize` | Number (e.g., `12`) | Font size in points | | `textAlign` | `left`, `center`, `right` | Horizontal text alignment | ### Text Field Options [#text-field-options] | Option | Values | Description | | ---------------- | ------ | ------------------------------------- | | `label` | Text | Label shown in the field | | `placeholder` | Text | Placeholder text shown before signing | | `text` | Text | Pre-filled text value | | `characterLimit` | Number | Maximum characters allowed | ### Number Field Options [#number-field-options] | Option | Values | Description | | -------------- | ------------- | --------------------- | | `value` | Number | Pre-filled value | | `minValue` | Number | Minimum allowed value | | `maxValue` | Number | Maximum allowed value | | `numberFormat` | Format string | Number display format | ### Examples with Options [#examples-with-options] ``` {{text, r1, required=true, label=Company Name}} {{number, r1, minValue=0, maxValue=100, value=50}} {{name, r1, fontSize=14}} {{text, r2, readOnly=true, text=Contract #12345}} ``` Signature and Free Signature fields do not support additional options beyond the field type and recipient. ## Multiple Recipients Example [#multiple-recipients-example] Here's how a document might look with placeholders for two signers: ``` AGREEMENT Party A Signature: {{signature, r1}} Party A Name: {{name, r1}} Party A Date: {{date, r1}} Party B Signature: {{signature, r2}} Party B Name: {{name, r2}} Party B Date: {{date, r2}} ``` When uploaded, this creates: * 3 fields assigned to recipient 1 (Party A) * 3 fields assigned to recipient 2 (Party B) * 2 placeholder recipients that you can update with real email addresses ## Tips for Creating Documents [#tips-for-creating-documents] 1. **Use a readable font** - Placeholders need to be readable by the PDF parser. Standard fonts like Arial, Helvetica, or Times New Roman work best. 2. **Don't split placeholders** - Ensure the entire placeholder text `{{...}}` is on a single line and not broken across text boxes. 3. **Size matters** - The field will be sized to match the placeholder text width. Use spaces or longer placeholder text if you need wider fields. 4. **Test with a draft** - Upload your document as a draft first to verify fields are detected correctly before sending. Placeholder detection happens automatically when you upload a PDF. You can review and adjust the created fields in the document editor before sending. ## Troubleshooting [#troubleshooting] * Ensure placeholders use double curly braces: `{{...}}` * Check that the placeholder includes a recipient identifier (e.g., `r1`) * Verify the field type is spelled correctly * Try using a standard font in your source document * The field is placed at the exact location of the placeholder text - If the position seems off, check that your PDF wasn't scaled or reformatted when exported * Placeholder text is covered with a white rectangle after field creation * If you see the text, try re-uploading the document ## See Also [#see-also] * [Add Fields to Documents](/docs/users/documents/add-fields) - Place fields manually on your documents * [Field Types](/docs/concepts/field-types) - Detailed reference for all field types * [Fields API](/docs/developers/api/fields) - Add fields programmatically via API # Recipient Expiration ## Overview [#overview] Recipient expiration lets you set a deadline for how long recipients have to sign a document after it is sent. Once the deadline passes, the recipient can no longer access the signing link and the document owner is notified. This is useful when: * A business deal is contingent on being signed within a specific time frame * A document is no longer relevant after a certain date * You want to ensure recipients act promptly rather than leaving documents unsigned indefinitely Expiration is tracked **per recipient**, not per document. If one recipient's deadline passes, other recipients can still sign. The document stays in a pending state so the owner can decide whether to resend or cancel. ## Default Behaviour [#default-behaviour] Every organisation has a default expiration period of **3 months**. This means that when you send a document, each recipient has 3 months from the time the document is sent to complete their signing. You can change this default at the organisation or team level, or override it per document. ## Settings Cascade [#settings-cascade] Expiration settings follow a three-level cascade: **Organisation → Team → Document**. Each level can override the one above it. Sets the default for all teams in the organisation. Options are a **custom duration** or **never expires**. To configure, navigate to **Organisation Settings > Preferences > Document** and find **Default Envelope Expiration**. Overrides the organisation default for documents created within this team. Options are a **custom duration**, **never expires**, or **inherit from organisation**. New teams default to **inherit from organisation**. To configure, navigate to **Team Settings > Preferences > Document** and find **Default Envelope Expiration**. Overrides the team or organisation default for a single document. Options are a **custom duration** or **never expires**. If you do not change the expiration when editing a document, the team or organisation default applies. ## Set Expiration for a Document [#set-expiration-for-a-document] {/* prettier-ignore */} ### Open the document settings [#open-the-document-settings] In the document editor, open the **Settings** dialog and go to the **General** tab. ### Configure the expiration [#configure-the-expiration] Find the **Expiration** field. Choose one of: * **Custom duration** — enter a number and select a unit (days, weeks, months, or years) * **Never expires** — the recipient can sign at any time If you leave it unchanged, the team or organisation default applies. Recipient Expiration Screenshot ### Send the document [#send-the-document] When you send the document, the expiration deadline is calculated from that moment. For example, if you set a 7-day expiration and send the document on March 1st, the recipient has until March 8th to sign. You cannot change the expiration period after the document has been sent. To extend a recipient's deadline, resend the document to them — this resets the clock. ## Set a Default Expiration Period [#set-a-default-expiration-period] {/* prettier-ignore */} ### Navigate to document preferences [#navigate-to-document-preferences] Go to **Organisation Settings > Preferences > Document** (or **Team Settings > Preferences > Document** for team-level overrides). ### Configure the default [#configure-the-default] Find **Default Envelope Expiration** and choose: * **Custom duration** — enter a number and unit * **Never expires** — no deadline for recipients * **Inherit from organisation** (team level only) — use whatever the organisation has configured ### Save [#save] Click **Save** to apply. New documents created after this change use the updated default. Changing the default expiration does not affect documents that have already been sent. Only new documents use the updated setting. ## What Happens When a Recipient Expires [#what-happens-when-a-recipient-expires] When a recipient's signing deadline passes: 1. The recipient can no longer access the signing link. They see a message explaining that the signing deadline has expired and to contact the document owner. 2. The document owner receives an email notification with a link to view the document. 3. An audit log entry is created recording the expiration. 4. The document remains in a **pending** state — other recipients who have not expired can still sign. Recipient Expired Signing Page ## Resending to Extend a Deadline [#resending-to-extend-a-deadline] If a recipient's deadline has passed (or is about to), you can resend the document to them. Resending recalculates the expiration from the current time, effectively extending the deadline. {/* prettier-ignore */} ### Open the document [#open-the-document] Navigate to the document page and find the recipient whose deadline has expired. Expired recipients are marked with an **Expired** badge. ### Resend [#resend] Click the resend option for the recipient. This sends a new signing link and resets the expiration clock based on the document's configured expiration period. ## Expiration Options Reference [#expiration-options-reference] | Unit | Example | Description | | ------ | -------- | --------------------------------------------------------------- | | Days | 7 days | Recipient has 7 days from when the document is sent | | Weeks | 2 weeks | Recipient has 2 weeks from when the document is sent | | Months | 3 months | Recipient has 3 months from when the document is sent (default) | | Years | 1 year | Recipient has 1 year from when the document is sent | You can also set expiration to **never expires**, which means the signing link remains valid indefinitely. *** ## See Also [#see-also] * [Send Documents](/docs/users/documents/send) - Send documents for signing * [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings * [Add Recipients](/docs/users/documents/add-recipients) - Add signers and other recipients to a document # Signing Reminders ## Overview [#overview] Signing reminders automatically email recipients who have not completed their signing. You configure when the first reminder goes out and how often it repeats — Documenso handles the rest until the recipient signs or the document leaves a pending state. This is useful when: * You want recipients nudged without having to track them down manually * A deadline is approaching and you want to escalate gradually * You send a high volume of documents and cannot chase each one Reminders are tracked **per recipient**, not per document. Each unsigned recipient is on their own schedule based on when they were emailed. This is different from the **Resend** action covered in [Send Documents](/docs/users/documents/send), which is a one-off manual nudge. Reminders are scheduled and recurring. ## Default Behaviour [#default-behaviour] Every **newly created** organisation starts with reminders **enabled**: * First reminder sent **5 days** after the recipient is emailed * Repeats **every 2 days** until the recipient signs You can change this default at the organisation or team level, or override it per document. Organisations created **before this feature shipped** have reminders left blank (no default) so recipients of in-flight or future documents are not unexpectedly sent reminders. To enable reminders for an existing organisation, configure **Default Signing Reminders** under **Organisation Settings > Preferences > Document**. ## Settings Cascade [#settings-cascade] Reminder settings follow a three-level cascade: **Organisation → Team → Document**. Each level can override the one above it, or inherit from it. Sets the default for all teams in the organisation. Options are **Enabled** (set when the first reminder fires and how often it repeats) or **No reminders**. To configure, navigate to **Organisation Settings > Preferences > Document** and find **Default Signing Reminders**. Overrides the organisation default for documents created within this team. Options are **Enabled**, **No reminders**, or **Inherit from organisation**. New teams default to **Inherit from organisation**. To configure, navigate to **Team Settings > Preferences > Document** and find **Default Signing Reminders**. Overrides the team or organisation default for a single document. Options are **Enabled**, **No reminders**, or **Inherit from organisation**. If you do not change reminders when editing a document, the team or organisation default applies. ## Set Reminders for a Document [#set-reminders-for-a-document] {/* prettier-ignore */} ### Open the document settings [#open-the-document-settings] In the document editor, open the **Settings** dialog and go to the **Reminders** tab. ### Choose a mode [#choose-a-mode] * **Enabled** — Documenso sends reminders on the schedule you configure below * **No reminders** — no automatic reminders for this document * **Inherit from organisation** — use the team or organisation default ### Configure the schedule [#configure-the-schedule] When **Enabled**, set: * **Send first reminder after** — a number and unit (days, weeks, or months) measured from when the recipient is first emailed * **Then repeat every** — either **Custom interval** (a number and unit) or **Don't repeat** (only one reminder is ever sent) ### Send the document [#send-the-document] The first reminder is scheduled when the recipient receives the initial signing email. Subsequent reminders are scheduled from the time the previous reminder was sent. Editing reminder settings on a document that is already pending recalculates the next reminder for every unsigned recipient immediately. ## Set a Default Reminder Schedule [#set-a-default-reminder-schedule] {/* prettier-ignore */} ### Navigate to document preferences [#navigate-to-document-preferences] Go to **Organisation Settings > Preferences > Document** (or **Team Settings > Preferences > Document** for team-level overrides). ### Configure the default [#configure-the-default] Find **Default Signing Reminders** and choose: * **Enabled** — set the first-reminder delay and repeat interval * **No reminders** — disable reminders by default * **Inherit from organisation** (team level only) — use whatever the organisation has configured ### Save [#save] Click **Save** to apply. New documents created after this change use the updated default. Documents already in flight are unaffected unless you edit their reminder settings directly. ## What Happens When a Reminder Fires [#what-happens-when-a-reminder-fires] When a recipient's next reminder time arrives: 1. Documenso sends the reminder email — same template as the original signing request, but the subject and preview are prefixed with **"Reminder:"**. 2. An audit log entry is created for the recipient (an `EMAIL_SENT` entry of type `REMINDER`). 3. The `document.reminder.sent` webhook fires. See [webhook events](/docs/developers/webhooks/events). 4. The next reminder is scheduled based on **Then repeat every**, or no further reminder is scheduled if you chose **Don't repeat**. Reminders stop automatically when the recipient signs, declines, the recipient's signing deadline passes, or the document leaves the pending state (completed, rejected, or deleted). ## When Reminders Are Skipped [#when-reminders-are-skipped] A configured reminder will not be sent in the following cases: * The recipient is a **CC** — CCs are notified once and never reminded * The recipient's **signing deadline has passed** — see [Recipient Expiration](/docs/users/documents/advanced/recipient-expiration). Resending the document refreshes the deadline and resumes reminders. * The document uses **manual link distribution** — Documenso never emails recipients for these documents * The envelope's email settings have **signing request emails disabled** * The recipient has not yet been emailed (for example, an unreached step in a sequential workflow) Reminders stop automatically **30 days after the recipient was first emailed**, regardless of the repeat interval. This hard cap prevents runaway reminder chains for recipients who never sign and have no expiration set. If you need a different stop condition, set a shorter repeat interval, use **Don't repeat**, or configure [recipient expiration](/docs/users/documents/advanced/recipient-expiration). Reminders are dispatched by a background sweep that runs every 15 minutes, so the actual send time may be up to \~15 minutes after the scheduled time. ## Reminder Options Reference [#reminder-options-reference] | Setting | Options | Notes | | ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | **Mode** | Enabled / No reminders / Inherit | Inherit is only available at the team and document levels | | **Send first reminder after** | 1+ days, weeks, or months | Measured from when the recipient receives the initial signing email. Reminders past 30 days from that moment are skipped. | | **Then repeat every** | Custom interval (1+ days/weeks/months) or Don't repeat | Custom interval keeps reminding until the recipient signs or the 30-day cap is reached | The organisation default out of the box is **first reminder after 5 days, repeating every 2 days**, which falls well inside the 30-day cap. *** ## See Also [#see-also] * [Send Documents](/docs/users/documents/send) - Send documents and trigger a one-off resend * [Recipient Expiration](/docs/users/documents/advanced/recipient-expiration) - Set a hard signing deadline * [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings * [Webhook Events](/docs/developers/webhooks/events) - Subscribe to `document.reminder.sent` # Branding Branding settings page ## Plan Requirements [#plan-requirements] Custom branding requires a **Teams plan or higher**. On lower plans, the branding settings page displays an upgrade prompt. ## What You Can Customize [#what-you-can-customize] | Setting | Description | | ----------------- | ----------------------------------------------------------------------------- | | **Logo** | Displayed on signing pages when recipients view and sign your documents | | **Website URL** | Makes your logo clickable, linking to your website | | **Brand Details** | Text shown at the bottom of email notifications (address, company info, etc.) | ## Limitations [#limitations] | Item | Limit / Requirement | | ----------------- | ------------------------------------------------- | | Logo file size | Maximum 5MB | | Supported formats | JPG, PNG, or WebP | | Brand details | Maximum 500 characters | | Website URL | Must be a valid URL (e.g., `https://example.com`) | ## Configure Branding [#configure-branding] {/* prettier-ignore */} ### Navigate to branding settings [#navigate-to-branding-settings] Go to your organisation settings and select the **Branding** tab under **Preferences**. For team branding, go to your team's settings page and select the **Branding** tab under **Preferences**. ### Enable custom branding [#enable-custom-branding] Select **Yes** from the **Enable Custom Branding** dropdown. When disabled, recipients see the default Documenso branding. ### Upload your logo [#upload-your-logo] Click the file input to select an image from your computer. A preview displays after selection. To remove an uploaded logo, click **Remove**. ### Add your website URL (optional) [#add-your-website-url-optional] Enter your website URL in the **Brand Website** field. When set, clicking your logo on the signing page opens your website in a new tab. ### Add brand details (optional) [#add-brand-details-optional] Enter company information in the **Brand Details** field. This text appears at the bottom of email notifications sent to recipients. Use this for: * Company address * Contact information * Legal disclaimers ### Save changes [#save-changes] Click **Update** to save your branding settings. ## Where Branding Appears [#where-branding-appears] | Location | Effect | | ----------------------- | ------------------------------------------------------------ | | **Signing pages** | Recipients see your logo when viewing and signing documents | | **Email notifications** | Brand details appear at the bottom of signing request emails | ## Organisation vs Team Branding [#organisation-vs-team-branding] Branding can be configured at two levels: ### Organisation branding [#organisation-branding] Set default branding for your entire organisation in **Organisation Settings > Preferences > Branding**. All teams inherit these settings by default. ### Team branding [#team-branding] Override organisation branding for specific teams in **Team Settings > Preferences > Branding**. Team settings support three options: | Setting | Behavior | | ----------------------------- | ---------------------------------------------------------------- | | **Yes** | Enable branding with team-specific logo and details | | **No** | Disable branding for this team (show default Documenso branding) | | **Inherit from organisation** | Use the organisation's branding settings | When a team has **Inherit from organisation** selected, any changes to organisation branding automatically apply to that team. For individual branding fields (logo, URL, details), leaving them blank also inherits from the organisation. ## Troubleshooting [#troubleshooting] * Verify custom branding is enabled (set to **Yes**) * Check that the image file is under 5MB * Ensure the file format is JPG, PNG, or WebP * Wait a few minutes for changes to propagate * Confirm the **Brand Details** field is filled in * Verify branding is enabled * Check that you saved changes by clicking **Update** *** ## See Also [#see-also] * [Profile Settings](/docs/users/settings/profile) - Update your name and public profile * [Security Settings](/docs/users/settings/security) - Configure two-factor authentication * [Create a Team](/docs/users/organisations/create-team) - Set up team-specific branding # Document ## Overview [#overview] Document preferences allow you to set the default settings when creating new documents and templates. For example, you can set the default language for documents sent by the team, or set the allowed signatures types. ## Preferences [#preferences] Document preferences can be set on either the organisation or team level. By default, teams inherit the preferences from the organisation. You can override these preferences on the team level at any time. To access the preferences, navigate to either the organisation or teams settings page and click the **Document** tab under the **Preferences** section. A screenshot of the organisation's document preferences page | Setting | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Default Document Visibility** | Default visibility for documents created by team members. See [document visibility](/docs/users/documents/advanced/document-visibility). | | **Default Document Language** | Default language for documents and for email communications with recipients. | | **Default Date Format** | Date format used for date fields and signing. | | **Default Time Zone** | Timezone used for date fields and signing. | | **Default Signature Settings** | Which signature types (typed, drawn, uploaded) recipients can use when signing. | | **Send on Behalf of Team** | Whether the sender's name appears in team emails. See [sender details](#sender-details) below. | | **Include the Signing Certificate** | Whether the signing certificate is embedded in signed PDFs. The certificate is always available separately from the logs page. | | **Include the Audit Logs** | Whether the audit logs are embedded in the document when downloaded. The audit logs are always available separately from the logs page. | | **Default Recipients** | Recipients that are automatically added to new documents. Can be overridden per document. | | **Default Envelope Expiration** | How long recipients have to sign before the signing link expires. See [recipient expiration](/docs/users/documents/advanced/recipient-expiration). | | **Default Signing Reminders** | When and how often to email recipients who have not yet signed. See [signing reminders](/docs/users/documents/advanced/signing-reminders). | | **Delegate Document Ownership** | By default, documents created with a team API token are owned by the user who created the token. Enable this setting to let supported API requests assign ownership to another team member. | | **AI Features** | Enable AI-powered features such as automatic recipient detection. Only shown if AI features are configured on the instance. | Document visibility, language, and signature settings can be overridden per document. ### Sender Details [#sender-details] If the **Sender Details** setting is enabled, the emails sent by the team will include the sender's name. The email will say: > "Example User" on behalf of "Example Team" has invited you to sign "document.pdf" If the **Sender Details** setting is disabled, the emails sent by the team will not include the sender's name. The email will say: > "Example Team" has invited you to sign "document.pdf" ## See Also [#see-also] * [Document Visibility](/docs/users/documents/advanced/document-visibility) - Control who can access documents * [Email Preferences](/docs/users/organisations/preferences/email) - Configure email notification settings * [Send Documents](/docs/users/documents/send) - Send documents for signing # Email ## Overview [#overview] Email preferences allow you to set the default settings when emailing documents to your recipients. ## Preferences [#preferences] Email preferences can be set on either the organisation or team level. By default, teams inherit the preferences from the organisation. You can override these preferences on the team level at any time. To access the preferences, navigate to either the organisation or teams settings page and click the **Email** tab under the **Preferences** section. A screenshot of the organisation's email preferences page | Setting | Description | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Default Email** | The sender email address used when sending documents. Requires a verified [email domain](/docs/users/organisations/email-domains). Teams can inherit from the organisation or select their own. | | **Reply to email** | Email address used in the "Reply To" field in outgoing emails. Leave blank to inherit from the organisation. | | **Default Email Settings** | Controls which emails are sent to recipients during document signing (e.g. signing invitations, completion notifications). Teams can inherit from the organisation or override with custom settings. | ## See Also [#see-also] * [Email Domains](/docs/users/organisations/email-domains) - Send emails from your own domain * [Document Preferences](/docs/users/organisations/preferences/document) - Configure other document defaults * [Send Documents](/docs/users/documents/send) - Customize emails when sending documents # Preferences Organisation preferences set defaults that teams inherit. Teams can override these settings individually. *** # Configuration SSO is only available on the Enterprise plan. ## Supported Providers [#supported-providers] Documenso supports any identity provider that implements the OpenID Connect (OIDC) protocol. Common providers include: * Microsoft Entra ID (formerly Azure AD) * Google Workspace * Okta * Auth0 * OneLogin * JumpCloud ## Setting Up SSO [#setting-up-sso] You must be an organisation admin or manager to configure SSO. {/* prettier-ignore */} ### Access SSO settings [#access-sso-settings] 1. Navigate to your organisation settings 2. Select **Single Sign-On** from the sidebar SSO settings page ### Get your redirect URI [#get-your-redirect-uri] At the top of the SSO settings page, you'll find the **Redirect URI** and **Required Scopes**. Copy these values - you'll need them when configuring your identity provider. ### Configure your identity provider [#configure-your-identity-provider] In your identity provider's admin console, create a new OIDC application with: * **Redirect URI**: The value from the Documenso SSO settings page * **Scopes**: `openid`, `email`, `profile` After creating the application, note down: * Client ID * Client Secret * OpenID Connect discovery URL (also called "well-known" URL) ### Enter SSO configuration [#enter-sso-configuration] Back in Documenso, enter the following: * **Issuer URL**: Your provider's OpenID Connect discovery endpoint * **Client ID**: The application client ID from your provider * **Client Secret**: The application secret from your provider ### Set allowed domains and default role [#set-allowed-domains-and-default-role] Configure which email domains can authenticate and what role new users receive (see sections below). ### Enable SSO [#enable-sso] Toggle **Enable SSO portal** and click **Update** to activate SSO for your team. ## SSO Configuration Settings [#sso-configuration-settings] ### Issuer URL [#issuer-url] The OpenID Connect discovery endpoint for your identity provider. | Provider | Issuer URL Format | | ------------------ | ------------------------------------------------------------------------------------- | | Google Workspace | `https://accounts.google.com/.well-known/openid-configuration` | | Microsoft Entra ID | `https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration` | | Okta | `https://{your-domain}.okta.com/.well-known/openid-configuration` | | Auth0 | `https://{your-domain}.auth0.com/.well-known/openid-configuration` | ### Client Credentials [#client-credentials] * **Client ID**: The unique identifier for your OIDC application * **Client Secret**: The secret key used to authenticate your application Store your client secret securely. If compromised, regenerate it immediately in your identity provider. ### Default Organisation Role [#default-organisation-role] The role assigned to users when they first sign in through SSO. You can change individual roles after they've joined. Can view and sign documents shared with them. Can manage documents and organisation settings. Full access to organisation configuration. ### Allowed Email Domains [#allowed-email-domains] Restrict which email domains can authenticate through your SSO portal. Enter domains separated by spaces: ``` example.com subsidiary.example.com ``` Leave empty to allow any domain authenticated by your identity provider. Without domain restrictions, anyone who can authenticate with your identity provider can join your team. ### Allow Personal Organisations [#allow-personal-organisations] Controls whether users signing in via SSO for the first time also receive their own personal organisation in addition to joining your organisation. * **Enabled**: New SSO users get a personal organisation where they can create and manage their own documents independently. * **Disabled**: New SSO users only join your organisation and do not receive a personal organisation. ## User Provisioning [#user-provisioning] When a user signs in through your SSO portal for the first time: {/* prettier-ignore */} ### Verify identity [#verify-identity] Documenso verifies their identity with your provider. ### Create or link account [#create-or-link-account] A new account is created, or linked if the email already exists. ### Add to organisation [#add-to-organisation] The user is added to your organisation with the default role. ### Grant access [#grant-access] They can immediately access organisation documents. Users provisioned through SSO: * Cannot change their email address * Cannot set a Documenso password (they must use SSO) * Are removed from SSO access if removed from your identity provider ## Signing In with SSO [#signing-in-with-sso] Once SSO is enabled, your organisation's SSO portal URL is displayed at the top of the SSO settings page. Share this URL with your members so they can sign in through your identity provider. SSO-only enforcement is not currently available. Users who have an existing Documenso account with a password can still sign in with their original method in addition to SSO. ## Troubleshooting [#troubleshooting] {/* prettier-ignore */} * Verify the URL is correct and publicly accessible * Ensure the URL points to an OpenID Connect discovery document * Check that the URL includes the full path (usually ending in `/.well-known/openid-configuration`) {/* prettier-ignore */} * Verify the client ID and secret match your identity provider exactly * Check that the OIDC application hasn't been deleted or disabled. * Regenerate the client secret if you suspect it's incorrect. {/* prettier-ignore */} * Copy the redirect URI exactly from Documenso's SSO settings * Check for trailing slashes. They must match exactly. * Ensure you're using HTTPS. {/* prettier-ignore */} * Verify the user's email domain is in the allowed domains list * Check that the user exists in your identity provider * Ensure the user's account in your identity provider has an email address {/* prettier-ignore */} If a user already has a Documenso account with the same email, their existing account will be linked to your team and they can sign in through your SSO portal going forward. ## Testing Your SSO Portal [#testing-your-sso-portal] After configuring SSO, verify it works correctly: {/* prettier-ignore */} ### Open portal [#open-portal] Navigate to your portal URL (found at the top of the organisation SSO settings page). ### Sign in [#sign-in] Sign in with a test account from your configured domain. ### Verify provisioning [#verify-provisioning] Verify the user is provisioned with the correct organisation role. ### Custom Subdomain [#custom-subdomain] To reduce friction for your users, create a custom subdomain that redirects to your SSO portal link. For example, `documenso.your-organisation.com` can redirect to the portal URL shown in your SSO settings. *** ## See Also [#see-also] * [Manage Members](/docs/users/organisations/members) - Configure team member roles and permissions * [Create a Team](/docs/users/organisations/create-team) - Set up additional teams in your organisation # Single Sign-On (SSO) SSO is only available on the Enterprise plan. Documenso supports any identity provider that implements the OpenID Connect (OIDC) protocol. *** # Microsoft Entra ID SSO is only available on the Enterprise plan. ## Overview [#overview] Microsoft Entra ID (formerly Azure Active Directory) is a popular identity provider for enterprise SSO. This guide will walk you through creating an app registration and configuring it for use with your Documenso SSO portal. ## Prerequisites [#prerequisites] * Access to Microsoft Entra ID (Azure AD) admin center * Access to your Documenso organisation as an administrator or manager Each user in your Azure AD will need an email associated with it. ## Creating an App Registration [#creating-an-app-registration] {/* prettier-ignore */} ### Access Azure portal [#access-azure-portal] * Navigate to the Azure Portal * Sign in with your Microsoft Entra ID administrator account * Search for "Azure Active Directory" or "Microsoft Entra ID" in the search bar * Click on "Microsoft Entra ID" from the results ### Create app registration [#create-app-registration] * In the left sidebar, click on "App registrations" * Click the "New registration" button ### Configure app registration [#configure-app-registration] Fill in the registration form with the following details: * **Name**: Your preferred name (e.g. `Documenso SSO Portal`) * **Supported account types**: Choose based on your needs * **Redirect URI (Web)**: Found in the Documenso SSO portal settings page Click "Register" to create the app registration. ### Get client ID [#get-client-id] After registration, you'll be taken to the app's overview page. The **Application (client) ID** is displayed prominently - this is your Client ID for Documenso. ### Create client secret [#create-client-secret] * In the left sidebar, click on "Certificates & secrets" * Click "New client secret" * Add a description (e.g., "Documenso SSO Secret") * Choose an expiration period (recommended 12-24 months) * Click "Add" Copy the **Secret value**, not the **Secret ID**. You won't be able to access it again after you leave the page. ## Getting Your OpenID Configuration URL [#getting-your-openid-configuration-url] {/* prettier-ignore */} ### Open Microsoft Entra ID [#open-microsoft-entra-id] In the Azure portal, go to **Microsoft Entra ID**. ### Go to overview [#go-to-overview] Click **Overview** in the left sidebar. ### Open endpoints [#open-endpoints] Click **Endpoints** in the horizontal tab. ### Copy the URL [#copy-the-url] Copy the **OpenID Connect metadata document** value. ## Configure Documenso SSO Portal [#configure-documenso-sso-portal] Now you have all the information needed to configure your Documenso SSO portal: | Field | Value | | ----------------- | ------------------------------------------------------------------- | | **Issuer URL** | The "OpenID Connect metadata document" value from the previous step | | **Client ID** | The Application (client) ID from your app registration | | **Client Secret** | The secret value you copied during creation | ## See Also [#see-also] * [Single Sign-On](/docs/users/organisations/single-sign-on) - General SSO configuration guide * [OAuth Providers (Self-Hosting)](/docs/self-hosting/configuration/advanced/oauth-providers) - Configure OAuth for self-hosted instances * [Security Settings](/docs/users/settings/security) - Manage account security options