Documenso is an open-source alternative to DocuSign: electronic signatures, templates, form fields, and an audit trail. Available as self-hosted or cloud (app.documenso.com). The key advantage is the absence of the $15-45/user/mo subscription typical of DocuSign and Adobe Sign. For companies with their own infrastructure, Documenso self-hosted combined with a Kommo integration delivers a complete cycle: create a document for signing from a deal card, receive status updates in Kommo, and archive the signed PDF.
Documenso REST API uses Bearer token authentication (Personal Access Token from account settings). Core operations: create a document from a template, send for signing, check status, and download the signed PDF. Webhooks are triggered on document status changes.
Documenso Template - a pre-built document with configured fillable fields. Equivalent to a DocuSign Template. When creating a signing request, the fields are populated with data from the CRM.
Self-Hosted vs Cloud
Documenso Cloud (app.documenso.com): paid, but significantly cheaper than DocuSign. API and webhooks are available on all plans.
Self-hosted: completely free, Docker Compose deployment in 30 minutes. Data is stored on your own infrastructure. Supports GDPR/HIPAA when configured correctly.
Integration Architecture
Kommo: deal -> stage "Send Contract"
-> Kommo webhook -> Your server
Your server
-> Get deal data (name, email, amount)
-> Documenso API: create document from template
-> Documenso API: add signer + prefill fields
-> Documenso API: send for signing
-> Kommo: write document_id to custom field
Client signs
-> Documenso webhook: document.completed
-> Your server -> Kommo: Closed Won + PDF link
Implementation: Creating and Sending a Document
import requests, os, hmac, hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
DS_TOKEN = os.environ["DOCUMENSO_API_TOKEN"]
DS_BASE = os.environ.get("DOCUMENSO_BASE", "https://app.documenso.com")
DS_TEMPLATE = os.environ["DOCUMENSO_TEMPLATE_ID"]
DS_WEBHOOK_SC = os.environ["DOCUMENSO_WEBHOOK_SECRET"]
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
SIGN_STAGE_ID = int(os.environ["KOMMO_SIGN_STAGE_ID"])
SIGNED_STAGE_ID = int(os.environ["KOMMO_SIGNED_STAGE_ID"])
KOMMO_CF_DOC_ID = int(os.environ["KOMMO_CF_DOCUMENSO_ID"])
DOCUMENSO_BASE = f"{DS_BASE}/api/v1"
DS_HDR = {"Authorization": f"Bearer {DS_TOKEN}", "Content-Type": "application/json"}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}
def get_lead_contact(lead_id: int) -> tuple[dict, dict]:
r = requests.get(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
params={"with": "contacts,custom_fields_values"},
)
lead = r.json()
contacts = lead.get("_embedded", {}).get("contacts", [])
contact = {}
if contacts:
rc = requests.get(
f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
headers=KOMMO_HDR,
params={"with": "custom_fields_values"},
)
contact = rc.json()
return lead, contact
def get_email(contact: dict) -> str:
for cf in contact.get("custom_fields_values", []) or []:
if cf.get("field_code") == "EMAIL":
vals = cf.get("values", [])
if vals:
return vals[0].get("value", "")
return ""
def create_documenso_doc(signer_name: str, signer_email: str, fields: dict) -> str:
# Step 1: create document from template
r = requests.post(
f"{DOCUMENSO_BASE}/templates/{DS_TEMPLATE}/create-document",
headers=DS_HDR,
json={
"title": f"Contract - {signer_name}",
"signers": [{
"name": signer_name,
"email": signer_email,
"role": "SIGNER",
}],
"formValues": fields, # prefill template fields
},
)
r.raise_for_status()
doc_id = r.json().get("documentId", "")
# Step 2: send for signing
requests.post(f"{DOCUMENSO_BASE}/documents/{doc_id}/send", headers=DS_HDR)
return str(doc_id)
def save_doc_id(lead_id: int, doc_id: str):
requests.patch(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
json={"custom_fields_values": [{
"field_id": KOMMO_CF_DOC_ID,
"values": [{"value": doc_id}],
}]},
)
@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
data = request.json or {}
for lead_data in data.get("leads", {}).get("status", []):
lead_id = lead_data.get("id")
new_status = lead_data.get("status_id")
if new_status != SIGN_STAGE_ID:
continue
lead, contact = get_lead_contact(lead_id)
email = get_email(contact)
if not email:
continue
signer_name = contact.get("name", "")
fields = {
"client_name": signer_name,
"deal_amount": str(lead.get("price", 0) or 0),
"kommo_lead_id": str(lead_id),
}
doc_id = create_documenso_doc(signer_name, email, fields)
save_doc_id(lead_id, doc_id)
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": lead_id,
"entity_type": "leads",
"note_type": "common",
"params": {"text": f"Documenso: document {doc_id} sent for signing to {email}"},
}],
)
return jsonify({"status": "ok"}), 200
Implementation: Webhook on Signing
def verify_documenso_sig(body: bytes, sig: str) -> bool:
computed = hmac.new(DS_WEBHOOK_SC.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, sig)
@app.route("/webhooks/documenso", methods=["POST"])
def documenso_webhook():
sig = request.headers.get("X-Documenso-Signature", "")
if DS_WEBHOOK_SC and not verify_documenso_sig(request.data, sig):
return jsonify({"error": "invalid signature"}), 401
event = request.json or {}
ev_type = event.get("event", "")
if ev_type not in ("document.completed", "document.declined"):
return jsonify({"status": "ignored"}), 200
doc = event.get("data", {})
doc_id = str(doc.get("id", ""))
# Find the deal by doc_id (search in formValues)
meta = doc.get("meta", {}) or {}
fields = doc.get("formValues", {}) or meta.get("formValues", {}) or {}
lead_id = fields.get("kommo_lead_id", "")
if not lead_id:
return jsonify({"status": "no_lead_id"}), 200
if ev_type == "document.completed":
# Download signed PDF URL
r_doc = requests.get(f"{DOCUMENSO_BASE}/documents/{doc_id}", headers=DS_HDR)
pdf_url = r_doc.json().get("downloadUrl", "") if r_doc.status_code == 200 else ""
requests.patch(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
json={"status_id": SIGNED_STAGE_ID},
)
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": int(lead_id),
"entity_type": "leads",
"note_type": "common",
"params": {"text": f"Documenso: contract signed. PDF: {pdf_url}"},
}],
)
else:
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": int(lead_id),
"entity_type": "leads",
"note_type": "common",
"params": {"text": "Documenso: signer declined the document. Please clarify the reason."},
}],
)
return jsonify({"status": "ok"}), 200
Self-Hosted Documenso: Docker Compose
version: "3"
services:
documenso:
image: documenso/documenso:latest
environment:
NEXTAUTH_SECRET: "your-secret"
NEXTAUTH_URL: "https://sign.yourcompany.com"
DATABASE_URL: "postgresql://user:pass@postgres:5432/documenso"
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS: "" # or path to certificate
ports:
- "3000:3000"
postgres:
image: postgres:15
environment:
POSTGRES_DB: documenso
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
After startup: DS_BASE=https://sign.yourcompany.com.
Cost vs DocuSign
| Documenso Cloud | DocuSign Essentials | |
|---|---|---|
| Price | ~$30/mo (5 users) | $15/user/mo |
| API | Yes | Yes (from Standard) |
| Self-hosted | Yes (free) | No |
| Audit trail | Yes | Yes |
| eIDAS | In development | Yes |
Who This Is For
Companies sensitive to eSign licensing costs that have the capacity for self-hosting. Especially agencies and startups processing 10+ contracts per month. If full eIDAS AES/QES compliance is required, Yousign or Scrive are currently a better fit.
A similar open-source integration is described for Kommo + Docuseal.
Frequently Asked Questions
Does Documenso sign documents with a timestamp?
Yes, Documenso supports Qualified Electronic Timestamp (QTS) through integration with a TSA (Time Stamp Authority). For self-hosted setups, you need to configure the TSA endpoint. In the cloud version, timestamping is included automatically.
How do I set up a custom domain in Documenso Cloud?
Documenso Cloud Enterprise allows you to use a custom domain for the signing page (sign.yourcompany.com). In self-hosted, this is configured via NEXTAUTH_URL. Branding - logo and colors - can be customized in Team Settings.
Does formValues work with existing PDF documents?
formValues only works with documents created through a Documenso template that has explicitly defined fields. If you need to fill in fields in an existing PDF, use Documenso Templates: create a template from the PDF, add Text fields with field names, and they will become available via the formValues API.
Summary
Kommo + Documenso - open-source eSign without vendor lock-in:
- Bearer token,
POST /api/v1/templates/{id}/create-document+send formValues.kommo_lead_idfor reverse webhook correlation- HMAC-SHA256 verification of
X-Documenso-Signature document.completed-> Closed Won + PDF link- Self-hosted: change
DOCUMENSO_BASE, API is identical
If you need an integration between Kommo and Documenso or another open-source eSign solution - describe your requirements to the Exceltic.dev team.