Zoho Sign is an EU-friendly electronic signature platform with servers in the Netherlands and eIDAS SES/AES compliance. There is no native integration with Kommo. The correct approach is the Zoho Sign REST API v1 with OAuth2 authentication via Zoho Accounts.
Integration scenario
- Deal moves to the “Contract” stage in Kommo
- Kommo webhook triggers the creation of a signing request from a template
- Zoho Sign sends the document to the contact for signing
- Webhook
document.completed-> signed PDF URL -> Note in Kommo - Deal automatically moves to the next stage
Zoho OAuth2: EU specifics
Zoho uses regional domains for OAuth. For EU accounts (Netherlands DC):
Authorization: https://accounts.zoho.eu/oauth/v2/auth
Token: https://accounts.zoho.eu/oauth/v2/token
API base: https://sign.zoho.eu/api/v1
For US accounts: zoho.com. A wrong domain results in invalid_client or access_denied.
Registering the application:
- Zoho Developer Console -> Create New Client -> Server-based Applications
- Specify the redirect URI (e.g.
https://your-service.com/zoho/callback) - Request scope:
ZohoSign.requests.CREATE,ZohoSign.requests.READ,ZohoSign.documents.READ
Obtaining a token (Authorization Code Flow):
import requests
ZOHO_CLIENT_ID = "your_client_id"
ZOHO_CLIENT_SECRET = "your_client_secret"
ZOHO_REFRESH_TOKEN = "your_refresh_token" # save after initial authorization
ZOHO_DC = "eu" # "com" for US
TOKEN_URL = f"https://accounts.zoho.{ZOHO_DC}/oauth/v2/token"
SIGN_BASE = f"https://sign.zoho.{ZOHO_DC}/api/v1"
def get_access_token() -> str:
r = requests.post(TOKEN_URL, data={
"refresh_token": ZOHO_REFRESH_TOKEN,
"client_id": ZOHO_CLIENT_ID,
"client_secret": ZOHO_CLIENT_SECRET,
"grant_type": "refresh_token",
})
r.raise_for_status()
return r.json()["access_token"]
The access token lives for 1 hour. The refresh token does not expire unless manually revoked.
Creating a signing request from a template
Zoho Sign supports templates with fields (signature, date, name). When creating a request, you substitute the client’s data.
Listing templates:
def list_templates(token: str) -> list:
r = requests.get(
f"{SIGN_BASE}/templates",
headers={"Authorization": f"Zoho-oauthtoken {token}"},
)
r.raise_for_status()
return r.json().get("templates", [])
The Zoho-oauthtoken header (not Bearer) is the standard for the entire Zoho API.
Creating a signing request:
def create_signing_request(token: str, template_id: str, contact: dict, deal: dict) -> str:
"""Send document for signing. Returns document request ID."""
payload = {
"templates": {
"template_id": template_id,
"actions": [
{
"action_type": "SIGN",
"recipient_name": f"{contact['first_name']} {contact['last_name']}",
"recipient_email": contact["email"],
"signing_order": 1,
"private_notes": f"Kommo Deal #{deal['id']}",
}
],
"notes": f"Deal: {deal['name']}",
},
"data": {
"field_data": {
"field_text_data": {
"CompanyName": contact.get("company", ""),
"ContractDate": "2026-05-27",
"DealAmount": str(deal.get("price", "")),
}
}
},
}
r = requests.post(
f"{SIGN_BASE}/templates/{template_id}/createdocument",
headers={
"Authorization": f"Zoho-oauthtoken {token}",
"Content-Type": "application/json",
},
json={"data": json.dumps(payload)}, # Zoho Sign requires JSON as a string in the "data" field
)
r.raise_for_status()
return r.json()["requests"]["request_id"]
Important detail: the request body is sent as multipart/form-data with a data field containing a JSON string. Standard json=... won’t work - you need data={"data": json.dumps(payload)}.
Webhook from Zoho Sign
Configure in Zoho Sign Settings > Webhooks:
- Event:
document.completed,document.declined - URL: your endpoint
- Secret: for HMAC verification
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
ZOHO_WEBHOOK_SECRET = "your_webhook_secret"
@app.route("/zoho-sign/webhook", methods=["POST"])
def zoho_sign_webhook():
signature = request.headers.get("X-Zoho-Webhook-Token", "")
body = request.get_data()
expected = hmac.new(
ZOHO_WEBHOOK_SECRET.encode(),
body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
data = request.json
event_type = data.get("notifications", {}).get("operation", "")
request_id = data.get("requests", {}).get("request_id", "")
signed_url = data.get("requests", {}).get("sign_url", "")
if event_type == "RequestCompleted":
# Extract Kommo deal ID from the note or saved mapping
deal_id = get_deal_id_by_request(request_id)
add_kommo_note(deal_id, f"Zoho Sign: document signed. PDF: {signed_url}")
advance_kommo_deal_stage(deal_id)
return "ok", 200
Downloading the signed PDF
After RequestCompleted you can download the PDF:
def download_signed_pdf(token: str, request_id: str) -> bytes:
r = requests.get(
f"{SIGN_BASE}/requests/{request_id}/pdf",
headers={"Authorization": f"Zoho-oauthtoken {token}"},
)
r.raise_for_status()
return r.content # PDF bytes
The PDF is uploaded to Kommo as an attachment via /api/v4/leads/{id}/notes with base64-encoded content.
Real case
An EU IT distributor sending 40-50 contracts per month was dispatching documents manually through the Zoho Sign UI and manually marking signatures in Kommo. The delay between signing and updating the CRM was 1-3 hours.
After the integration:
- The document is sent to the client automatically when the deal moves to “Contract”
- Signing is reflected in Kommo within 10 seconds via webhook
- The PDF link is added as a Note without any manager involvement
In a typical month - about 4 hours of routine work saved across 45 contracts.
Who this is for
Companies in the EU that use or are considering Zoho Sign as part of the Zoho ecosystem. If the team is already on Zoho CRM/Books/Sign - authentication via a single OAuth2 application covers all products.
For DocuSign - see Kommo + DocuSign. For an open-source solution - Kommo + Docuseal.
Frequently asked questions
What eIDAS level does Zoho Sign support?
Zoho Sign supports Simple Electronic Signature (SES) by default for most documents. Advanced Electronic Signature (AES) is available through integration with Aadhaar or European trust services. For EU B2B contracts, SES is sufficient in most jurisdictions - AES is required for real estate and HR documents in certain countries.
How does template field mapping work?
Fields in the template are defined through the Zoho Sign Template Editor. Each text field has a name (CompanyName, ContractDate, etc.). In the API you pass field_text_data with these names as keys. Signatures and initials are not passed via API - they are filled in by the recipient in the Zoho Sign UI.
What if the client doesn’t sign within N days?
Set expiry_days when creating the request (maximum 90 days). Upon expiry, Zoho Sign sends a document.expired webhook. The handler can create a task in Kommo for the manager and send a new request if needed.
Can a document be sent to multiple signers?
Yes. In actions, specify multiple objects with signing_order 1, 2, 3. Zoho Sign will send the document sequentially or in parallel depending on the configuration. The document.completed webhook fires only when all signatures have been collected.
Summary
Key integration points for Kommo + Zoho Sign:
- Zoho OAuth2: regional domain (
zoho.eufor EU),Zoho-oauthtokenheader - Request from template: body passed as a JSON string in the
datafield of multipart - Webhook: HMAC-SHA256 via
X-Zoho-Webhook-Token - PDF download via
/api/v1/requests/{id}/pdf
If your team works with Zoho products and needs an integration with Kommo - describe your stack to the Exceltic.dev team. We’ll figure out what data needs to be synced and between which systems.