Kommo + Skribble: Qualified Electronic Signature EU/Switzerland from the Pipeline

Kommo + Skribble: Qualified Electronic Signature EU/Switzerland from the Pipeline

Skribble is a Swiss electronic signature platform with support for Qualified Electronic Signature (QES) under the EU eIDAS and Swiss ZertES standards. QES is the only signature type that carries the same legal weight as a handwritten signature across the EU. This is critical for contracts in Germany, Austria, Switzerland, and France, where electronic signature requirements are stricter than in the US or UK.

For B2B companies operating in the EU and signing contracts with corporate clients, Skribble + Kommo solves the problem end-to-end: send a document for QES signing directly from the deal card, receive the signed status back in Kommo, and archive the signed PDF.

Skribble API uses Bearer token authentication. Core operations: POST /v2/signature-requests - create a signature request, upload a document, and add signers. Webhooks: signature-request-signed, signature-request-declined.

QES (Qualified Electronic Signature) - the highest level of electronic signature under eIDAS. Requires personal identity verification via a video call with an agent or eMedId. Carries the same legal force as a handwritten signature in 30+ European countries.

Signature Levels in Skribble

LevelStandardLegal WeightIdentification
EES (Simple)eIDASBasicEmail
AES (Advanced)eIDASHighSMS/2FA
QES (Qualified)eIDAS/ZertESMaximum (= handwritten)Video identification

For most B2B contracts, AES is sufficient. QES is required for legally significant documents: employment contracts, credit agreements, and certain M&A documents.

Architecture

Kommo: deal -> stage "Send Contract"
  -> Kommo webhook -> Your server

Your server
  -> Load PDF template / retrieve from storage
  -> Skribble API: POST /v2/signature-requests
     {title, content (base64 PDF), signers, signature_quality: "AES"}
  -> Kommo: save signature_request_id

Client identifies themselves and signs
  -> Skribble webhook: signature-request-signed
  -> Your server -> Kommo: Closed Won + download PDF

Implementation

import requests, os, base64, hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

SKRIBBLE_API_KEY = os.environ["SKRIBBLE_API_KEY"]
SKRIBBLE_BASE    = "https://api.skribble.com/v2"
SKRIBBLE_HDR     = {"Authorization": f"Bearer {SKRIBBLE_API_KEY}", "Content-Type": "application/json"}
SKRIBBLE_SECRET  = os.environ.get("SKRIBBLE_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_SR_ID    = int(os.environ["KOMMO_CF_SIGNATURE_REQUEST_ID"])

KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

CONTRACT_PDF_PATH = os.environ.get("CONTRACT_TEMPLATE_PATH", "contract_template.pdf")

def get_lead_contact_email(lead_id: int) -> tuple[dict, str, str]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    lead     = r.json()
    contacts = lead.get("_embedded", {}).get("contacts", [])
    if not contacts:
        return lead, "", ""
    rc = requests.get(
        f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
        headers=KOMMO_HDR,
        params={"with": "custom_fields_values"},
    )
    c     = rc.json()
    email = ""
    for cf in c.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            if vals:
                email = vals[0].get("value", "")
                break
    return lead, c.get("name", ""), email

def create_signature_request(pdf_path: str, signer_name: str, signer_email: str,
                              title: str, lead_id: int) -> str:
    with open(pdf_path, "rb") as f:
        pdf_b64 = base64.b64encode(f.read()).decode()

    r = requests.post(
        f"{SKRIBBLE_BASE}/signature-requests",
        headers=SKRIBBLE_HDR,
        json={
            "title":     title,
            "message":   f"Please sign the contract. The link is valid for 14 days.",
            "content":   pdf_b64,
            "signers":   [{
                "email":           signer_email,
                "name":            signer_name,
                "signature_type":  "AES",  # or "QES" for qualified
                "language":        "de",   # de/en/fr/it
            }],
            "meta": {"kommo_lead_id": str(lead_id)},
        },
    )
    r.raise_for_status()
    return r.json().get("id", "")

def save_sr_id(lead_id: int, sr_id: str):
    requests.patch(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        json={"custom_fields_values": [{
            "field_id": KOMMO_CF_SR_ID,
            "values":   [{"value": sr_id}],
        }]},
    )

def add_note(lead_id: int, text: str):
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{
            "entity_id":   lead_id,
            "entity_type": "leads",
            "note_type":   "common",
            "params":      {"text": text},
        }],
    )

@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, name, email = get_lead_contact_email(lead_id)
        if not email:
            continue

        title = f"Contract - {name} - #{lead_id}"
        sr_id = create_signature_request(CONTRACT_PDF_PATH, name, email, title, lead_id)
        save_sr_id(lead_id, sr_id)
        add_note(lead_id, f"Skribble: signature request {sr_id} sent to {email}.")

    return jsonify({"status": "ok"}), 200

@app.route("/webhooks/skribble", methods=["POST"])
def skribble_webhook():
    event    = request.json or {}
    ev_type  = event.get("event_type", "")

    meta     = event.get("signature_request", {}).get("meta", {}) or {}
    lead_id  = meta.get("kommo_lead_id", "")
    sr_id    = event.get("signature_request", {}).get("id", "")

    if not lead_id:
        return jsonify({"status": "no_lead_id"}), 200

    if ev_type == "signature-request-signed":
        # Download signed PDF
        r_pdf = requests.get(
            f"{SKRIBBLE_BASE}/signature-requests/{sr_id}/download",
            headers=SKRIBBLE_HDR,
        )
        pdf_url = ""
        if r_pdf.status_code == 200:
            save_path = f"signed_{lead_id}_{sr_id}.pdf"
            with open(save_path, "wb") as f:
                f.write(r_pdf.content)
            pdf_url = f"(PDF saved: {save_path})"

        requests.patch(
            f"{KOMMO_BASE}/leads/{lead_id}",
            headers=KOMMO_HDR,
            json={"status_id": SIGNED_STAGE_ID},
        )
        add_note(int(lead_id), f"Skribble: contract signed. {pdf_url}")

    elif ev_type == "signature-request-declined":
        add_note(int(lead_id), "Skribble: the signer declined the signature request. Please follow up to clarify the reason.")

    return jsonify({"status": "ok"}), 200

Signature Quality: AES vs QES

For most B2B contracts (NDAs, service agreements, SaaS terms), signature_type: "AES" is sufficient. The client identifies themselves via SMS or email.

QES ("QES") requires prior video identification through Skribble ID or eMedId. The signer completes identification once - then signs without any additional steps going forward. Use QES for employment contracts in Germany (§ 623 BGB), credit agreements, and documents requiring notarization.

Languages and White Labeling

Skribble supports language: "de"/"en"/"fr"/"it" for email notifications sent to signers. The Enterprise plan includes custom branding for the signing page. For multilingual teams: determine the language from the contact’s custom field in Kommo.

Who This Is For

B2B companies with clients in DACH (Germany, Austria, Switzerland), France, and Scandinavia - especially where contracts require eIDAS-compliant signatures to carry legal force within the EU. Skribble is one of the few providers with native support for Swiss ZertES (Swiss electronic signature legislation).

Similar eSign integrations: Kommo + Documenso (open-source), Kommo + Yousign (French platform).

Frequently Asked Questions

How does Skribble store signed documents?

Skribble stores signed PDFs with an embedded digital signature and audit trail. Documents are accessible via API and Dashboard indefinitely (on the Enterprise plan). It is also recommended to download and store copies in your own S3 or storage solution - this way you are not dependent on Skribble to access your archive.

Can documents be signed by multiple signers?

Yes. Pass a list of signers in the signers field - Skribble will send each one a personal signing link. You can configure the signing order: sequential (one after another) or parallel (all at once). The signature-request-signed webhook fires when ALL signers have signed.

How does Skribble integrate with DATEV for German clients?

Skribble has a native connector with DATEV DMS (Document Management System). Signed documents are automatically pushed to DATEV. For international B2B teams working with German clients, this simplifies collaboration with German tax advisors. Configuration is done through the Skribble Dashboard without any API work.

Summary

Kommo + Skribble - QES/AES signing for EU deals, directly from the pipeline:

  • Bearer token, POST /v2/signature-requests with base64 PDF and meta.kommo_lead_id
  • signature_type: "AES" for most contracts, "QES" for employment and credit agreements
  • Webhook signature-request-signed -> download PDF -> Kommo Closed Won
  • Support for eIDAS (EU) and ZertES (Switzerland) - highest legal validity
  • language: "de"/"fr"/"en" for multilingual teams

If you need Kommo integration with Skribble or another EU-compliant eSign provider - describe your requirements to the Exceltic.dev team.

More articles

All →