Kommo + Oneflow: Automatic Contract Sending from the Sales Pipeline

Kommo + Oneflow: Automatic Contract Sending from the Sales Pipeline

Oneflow is a Swedish contract lifecycle management (CLM) platform: creation, negotiation, electronic signature, and storage. Unlike PandaDoc or DocuSign, Oneflow specialises in interactive HTML contracts (not PDF) — the client can edit specific fields directly in the browser before signing. GDPR-compliant, data stored in the EU. Without Kommo integration, a manager creates contracts manually. With the integration, Won -> contract populated with deal data is sent to the client in seconds.

Oneflow vs PandaDoc vs Docuseal for EU Teams

PlatformFormatData StorageEU FocusAPI
OneflowHTML contractsSweden / EUYes, Swedish vendorREST + webhooks
PandaDocPDF + rich contentUS (AWS)PartialREST
DocusealPDFSelf-hosted or EU cloudYes (self-hosted)REST
DocuSignPDFUSNo EU cloudREST

Oneflow is chosen by EU teams with data residency requirements and where contracts are not just a signature form but a negotiation tool (built-in chat, redlining, versioning).

What Gets Synchronised

Kommo -> Oneflow: — Won -> create a contract from a template with deal data (name, email, company, amount, plan) — Won -> set participants (client -> internal signatory) — Won -> send contract for signing

Oneflow -> Kommo:contract.signed (all parties signed) -> Note + stage change to “Contract Signed” — contract.participant_signed (one participant signed) -> Note — contract.declined -> Note + task: “Client declined the contract” — contract.expired -> Note + task: “Signing deadline expired”

Oneflow API: Key Requests

Base URL: https://api.oneflow.com/v1. Authentication: x-oneflow-api-token: {api_token} (personal token from settings). Additionally: x-oneflow-user-email: {email} — the user on whose behalf we act.

import requests

ONEFLOW_TOKEN = "your_api_token"
ONEFLOW_USER_EMAIL = "sales@yourcompany.com"
ONEFLOW_BASE_URL = "https://api.oneflow.com/v1"

HEADERS = {
    "x-oneflow-api-token": ONEFLOW_TOKEN,
    "x-oneflow-user-email": ONEFLOW_USER_EMAIL,
    "Content-Type": "application/json",
}

def get_templates(workspace_id: str) -> list:
    resp = requests.get(
        f"{ONEFLOW_BASE_URL}/templates",
        headers=HEADERS,
        params={"workspace_id": workspace_id}
    )
    resp.raise_for_status()
    return resp.json().get("data", [])

def create_contract(template_id: str, workspace_id: str,
                    name: str, parties: list) -> dict:
    # parties: list of company/individual participants with roles
    payload = {
        "template_id": template_id,
        "workspace_id": workspace_id,
        "name": name,
        "parties": parties,
    }
    resp = requests.post(
        f"{ONEFLOW_BASE_URL}/contracts",
        headers=HEADERS,
        json=payload
    )
    resp.raise_for_status()
    return resp.json()

def update_contract_data_fields(contract_id: str, data_fields: list) -> None:
    # Fill in custom contract fields (plan, amount, date)
    for field in data_fields:
        resp = requests.patch(
            f"{ONEFLOW_BASE_URL}/contracts/{contract_id}/data_fields/{field['id']}",
            headers=HEADERS,
            json={"value": field["value"]}
        )
        resp.raise_for_status()

def publish_contract(contract_id: str) -> dict:
    # Send contract to participants for signing
    resp = requests.post(
        f"{ONEFLOW_BASE_URL}/contracts/{contract_id}/publish",
        headers=HEADERS,
        json={}
    )
    resp.raise_for_status()
    return resp.json()

def on_deal_won(lead: dict, contact: dict):
    email = get_contact_email(contact)
    name = contact["name"]
    company = get_custom_field(lead, COMPANY_FIELD_ID) or name
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "Growth"
    amount = lead.get("price", 0)
    from datetime import datetime
    today = datetime.now().strftime("%d.%m.%Y")

    # Participant structure: client + our company
    parties = [
        {
            "type": "company",
            "name": company,
            "participants": [{
                "name": name,
                "email": email,
                "signatory": True,
            }]
        },
        {
            "type": "company",
            "name": "Your Company Name",
            "participants": [{
                "name": INTERNAL_SIGNER_NAME,
                "email": INTERNAL_SIGNER_EMAIL,
                "signatory": True,
            }]
        }
    ]

    contract = create_contract(
        template_id=ONEFLOW_TEMPLATE_ID,
        workspace_id=ONEFLOW_WORKSPACE_ID,
        name=f"Agreement with {company} - {plan}",
        parties=parties,
    )
    contract_id = contract["id"]

    # Fill template fields (get field IDs from Oneflow template editor)
    data_fields = [
        {"id": FIELD_PLAN_ID,    "value": plan},
        {"id": FIELD_AMOUNT_ID,  "value": str(amount)},
        {"id": FIELD_DATE_ID,    "value": today},
        {"id": FIELD_COMPANY_ID, "value": company},
    ]
    update_contract_data_fields(contract_id, data_fields)

    # Publish (send to participants)
    publish_contract(contract_id)

    update_kommo_deal(lead["id"], {"oneflow_contract_id": str(contract_id)})
    create_kommo_note(lead["id"],
        f"Oneflow: contract #{contract_id} sent for signing -> {email}")

Handling Oneflow Webhook:

from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/oneflow", methods=["POST"])
def oneflow_webhook():
    payload = request.json
    event_type = payload.get("event_type")
    contract_id = str(payload.get("contract_id", ""))

    deal_id = find_deal_by_field("oneflow_contract_id", contract_id)
    if not deal_id:
        return "", 200

    if event_type == "contract.signed":
        update_kommo_deal(deal_id, {"stage_id": STAGE_CONTRACT_SIGNED})
        create_kommo_note(deal_id, "Oneflow: all parties have signed the contract")

    elif event_type == "contract.participant_signed":
        signer = payload.get("participant", {}).get("name", "")
        create_kommo_note(deal_id, f"Oneflow: {signer} signed the contract")

    elif event_type == "contract.declined":
        participant = payload.get("participant", {}).get("name", "")
        create_kommo_note(deal_id,
            f"Oneflow: {participant} declined the contract")
        create_kommo_task(deal_id,
            "Clarify objections - contract declined in Oneflow")

    elif event_type == "contract.expired":
        create_kommo_note(deal_id, "Oneflow: contract signing deadline expired")
        create_kommo_task(deal_id, "Send a new version of the contract")

    return "", 200

Setting Up Webhooks in Oneflow: Settings -> Integrations -> Webhooks -> Add webhook. Specify the URL and select the events.

Interactive HTML Contracts: Oneflow’s Key Differentiator

In Oneflow, a contract is not a PDF but an HTML document. The client can: — Comment on specific clauses directly in the browser — Edit agreed fields (if permitted by the template) — View the change history

For lengthy B2B negotiations this is critical: instead of sending 5 PDF versions — one live document with versioning. Once negotiations are complete — one “Sign” button.

Real-World Case

IT consulting firm (Sweden, 20–30 contracts per month, Kommo + Oneflow):

  • Before: PandaDoc + manual creation. Contracts were sent to clients 2–3 hours after Won. 40% of contracts came back with revision requests — leading to email threads with PDF versions.
  • After: Won -> contract via Oneflow in 30 seconds. The client edits disputed clauses directly in the browser — email version chains disappeared. Average time from Won to signing: 1.3 days (was 4.7 days).
  • Additionally: EU GDPR — all contracts stored on servers in Sweden. A strong argument when working with EU clients who have DPA requirements.

Who This Is Relevant For

  • EU companies with GDPR requirements for contract data storage
  • B2B with a negotiation process: lengthy contracts, redlining, multiple clauses
  • Agencies and consultancies with 10+ contracts per month
  • Teams tired of “send PDF, get revisions, send again”

Frequently Asked Questions

Is Oneflow legally valid in the EU?

Yes. Signatures via Oneflow comply with eIDAS as a Simple Electronic Signature (SES). For a Qualified Electronic Signature (QES) — an additional ID provider is required. For most B2B contracts, SES is sufficient.

Where can I find the Template ID and Field ID in Oneflow?

Template ID: Oneflow Dashboard -> Templates -> select the template -> URL (/templates/{id}). Field ID: open the template in the editor -> select the data field -> the ID is visible in the right panel. Or via GET /templates/{id} — it returns all fields with their IDs.

Does Oneflow support multiple signatories and signing order?

Yes. In participants you can specify signatory_order for each participant — the contract is sent to the next person only after the previous one has signed. Parallel signing (without order) is the default.

How do I get the signed PDF from Oneflow via API?

GET /contracts/{id}/pdf -> returns the PDF with signatures. It is convenient to automatically save this to cloud storage or attach it to the deal in Kommo as a file.

Summary

  • Oneflow API: x-oneflow-api-token + x-oneflow-user-email in headers
  • Create contract: POST /contracts -> update data fields -> POST /contracts/{id}/publish
  • Webhook: events contract.signed, contract.participant_signed, contract.declined
  • HTML contracts: negotiation, redlining, version history — an advantage over PDF-based tools
  • EU data residency: data in Sweden, GDPR-compliant out of the box

If you use Oneflow and Kommo and want to automate contract creation upon Won — describe your template structure and signatory order. Exceltic.dev will configure the integration.

More articles

All →