Kommo + Proposify: Sales Proposals from the Deal Card

Proposify is a platform for creating interactive sales proposals with electronic signatures, view tracking, and a REST API. Without Kommo integration, after a demo the manager manually opens Proposify, copies the name, company, and amount from the CRM, and selects a template. With the integration, a button click in the deal card creates a proposal with pre-filled variables and sends it to the client — and upon signing, it automatically moves the deal to Won.

Why Native Tools Are Not Enough

Proposify integrates natively with HubSpot, Pipedrive, Salesforce, and Zoho through built-in connectors. Kommo is not on this list. Zapier offers a basic “proposal signed” trigger but cannot pass custom Kommo fields as proposal variables and does not support two-way status synchronisation.

The problem runs deeper architecturally: variables in a Proposify template ({{client_name}}, {{deal_amount}}, {{plan}}) must be populated at document creation time — from the specific Kommo deal’s fields, including custom fields. This requires a direct call to the Proposify API with parameters from the Kommo API. No no-code connector handles this correctly.

Proposify is an interactive proposal service where the client can review the proposal online, leave a comment, and sign electronically. Every client action (opened, viewed section, signed) generates a webhook event.

What the Kommo + Proposify Combination Delivers

Without integration: — After the demo, the manager manually opens Proposify and copies data from Kommo — Average time from demo to proposal send: 20–40 minutes — When signed, Proposify does not notify Kommo — the deal is updated manually or forgotten — The Sales Director cannot see in the CRM when the client opened and read the proposal

With integration: — Button in the deal card -> proposal created and sent in 30 seconds — Contact name, company, plan, and amount are populated automatically from deal fields — proposal.viewed -> Note in Kommo: “Client opened the proposal — {date}” — proposal.signed -> deal moves to Won, task for accountant to raise invoice — proposal.declined -> manager task + custom field with reason

What Gets Synchronised

Kommo -> Proposify: — Contact name and email -> variables {{client_name}}, {{client_email}} — Company name -> {{company_name}} — Deal amount -> {{deal_amount}} — Plan from custom field -> {{plan}}, {{billing_period}} — Deal ID -> custom attribute for reverse traceability

Proposify -> Kommo:proposal.viewed -> Note with viewing time — proposal.signed -> Won + Note “Proposal signed” + invoice task — proposal.declined -> custom field proposal_status = declined + manager task — Proposal URL -> custom field in deal (for quick access)

Architecture

Kommo: manager clicks "Create Proposal" button (custom widget button)
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> name, email, company, plan, amount
  2. Proposify: POST /proposals
     -> template_id + variables (client_name, deal_amount, plan...)
     -> get proposal_id + proposal_url
  3. Kommo: PATCH /leads/{id}
     -> custom field proposal_url = proposal link
     -> custom field proposal_status = sent
  4. Kommo: POST /leads/{id}/notes
     -> "Proposal sent to client: {proposal_url}"

Proposify Webhook: proposal.viewed
  ↓ Backend
  1. From payload: proposal_id, viewed_at, contact_email
  2. Find deal_id by proposal_id (saved in step 3 above)
  3. Kommo: POST /leads/{deal_id}/notes
     -> "Client opened the proposal: {viewed_at}"

Proposify Webhook: proposal.signed
  ↓ Backend
  1. From payload: proposal_id, signed_at, signer_name
  2. Find deal_id
  3. Kommo: PATCH /leads/{deal_id}
     -> pipeline_id + status_id -> Won
     -> proposal_status = signed
  4. Kommo: POST /tasks
     -> task: "Raise invoice - proposal signed {signed_at}"

Proposify REST API: Key Requests

Base URL: https://app.proposify.com/api/v2/. Authentication: Bearer token in the Authorization: Bearer {API_KEY} header.

Create a proposal from a template:

import requests

PROPOSIFY_API_KEY = 'your_api_key'
PROPOSIFY_BASE = 'https://app.proposify.com/api/v2'

def create_proposal(template_id: str, variables: dict,
                    recipient_email: str, recipient_name: str) -> dict:
    headers = {
        'Authorization': f'Bearer {PROPOSIFY_API_KEY}',
        'Content-Type': 'application/json'
    }

    payload = {
        'document_template_id': template_id,
        'recipients': [{
            'email': recipient_email,
            'name': recipient_name,
            'role': 'client'
        }],
        'variables': variables  # {'{client_name}': 'John Smith', '{plan}': 'Pro'}
    }

    resp = requests.post(
        f'{PROPOSIFY_BASE}/proposals',
        json=payload,
        headers=headers
    )
    resp.raise_for_status()
    data = resp.json()
    return {
        'proposal_id': data['proposal']['id'],
        'proposal_url': data['proposal']['url']
    }

Handling Proposify webhook:

import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'

def verify_proposify_signature(payload_bytes: bytes, signature: str) -> bool:
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload_bytes,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhooks/proposify', methods=['POST'])
def proposify_webhook():
    signature = request.headers.get('X-Proposify-Signature', '')
    if not verify_proposify_signature(request.data, signature):
        return '', 401

    payload = request.json
    event_type = payload.get('event')  # 'proposal.signed', 'proposal.viewed', etc.
    proposal = payload.get('proposal', {})
    proposal_id = proposal.get('id')

    deal_id = get_deal_id_by_proposal(proposal_id)  # from your DB or custom field
    if not deal_id:
        return '', 200  # proposal not from Kommo - ignore

    if event_type == 'proposal.signed':
        signed_at = proposal.get('signed_at')
        # Move deal to Won
        move_kommo_deal_to_won(deal_id)
        # Create Note
        create_kommo_note(deal_id,
            f'Proposify proposal signed {signed_at}. '
            f'Signer: {proposal.get("signer_name")}')
        # Invoice task
        create_kommo_task(deal_id, 'Proposal signed - raise invoice')

    elif event_type == 'proposal.viewed':
        viewed_at = proposal.get('viewed_at')
        create_kommo_note(deal_id,
            f'Client opened the Proposify proposal: {viewed_at}')

    elif event_type == 'proposal.declined':
        reason = proposal.get('decline_reason', 'not specified')
        update_kommo_deal(deal_id, {'proposal_status': 'declined'})
        create_kommo_task(deal_id,
            f'Client declined the proposal. Reason: {reason}')

    return '', 200

Verification: Proposify signs every webhook via HMAC-SHA256, passing the signature in the X-Proposify-Signature header. Always verify the signature before processing — this protects against forged requests.

Idempotency: Proposify may retry a webhook on timeout. Save processed proposal_id + event_type combinations and skip duplicates.

Kommo -> Proposify Variable Mapping

def build_proposify_variables(lead: dict, contact: dict) -> dict:
    custom_fields = {cf['field_id']: cf.get('values', [{}])[0].get('value')
                     for cf in lead.get('custom_fields_values', [])}

    PLAN_FIELD_ID = 123456   # ID of "Plan" custom field
    PERIOD_FIELD_ID = 123457  # ID of "Billing Period" field

    return {
        '{client_name}': contact.get('name', ''),
        '{client_email}': next(
            (e['value'] for e in contact.get('custom_fields_values', [])
             if e.get('field_code') == 'EMAIL'), ''),
        '{company_name}': contact.get('company', {}).get('name', ''),
        '{deal_amount}': str(lead.get('price', 0)),
        '{plan}': custom_fields.get(PLAN_FIELD_ID, 'Pro'),
        '{billing_period}': custom_fields.get(PERIOD_FIELD_ID, 'monthly'),
        '{deal_id}': str(lead.get('id'))
    }

Variables in Proposify templates are created in the editor: insert {{name}} in the text — they will then be available via the API. Variable names must match the keys in the variables dictionary.

Real-World Case

B2B SaaS (DACH region, 25–30 new qualified leads per month, Kommo + Proposify + Stripe):

  • Before: after a demo, a manager spent 30–40 minutes preparing a proposal. Errors in amounts and plans due to manual copying. When signed, Kommo was updated with a 1–2 day delay.
  • After: “Send proposal” button in the deal card -> proposal with the client in 30 seconds. Data is populated from deal fields — no errors. Upon signing, Kommo automatically moves to Won and the accountant receives a task to raise an invoice.
  • Additionally: the Sales Director can see in the deal timeline when the client opened the proposal — and how many times. This changes the follow-up approach: calling one hour after the first viewing is more effective than calling the next day.

A similar pattern applies to PandaDoc and DocuSign — the same webhook + template variable principles apply.

Who This Is Relevant For

  • B2B companies sending 15+ proposals per month
  • Those using Proposify as their primary tool for sales proposals
  • Companies that want to see proposal status directly in Kommo without switching to Proposify
  • Workflows where: demo -> proposal -> signing -> Won must be automatic

Frequently Asked Questions

Is the Proposify API public or enterprise-only?

The Proposify public API is available to users on the Business plan and above. An API Key is created in account settings: Settings -> Integrations -> API. Authentication via Bearer token in the request header.

How do I set up a webhook in Proposify?

Settings -> Integrations -> Webhooks -> Add Webhook URL. Select events: proposal.viewed, proposal.signed, proposal.declined. Proposify signs the payload via HMAC-SHA256 — the secret key is set there and used to verify requests on your server.

Can proposals be created automatically without a button click — on stage change?

Yes — via a Kommo webhook on status change. When a deal moves to the “Commercial Proposal” stage, the backend automatically creates and sends the proposal. This works when the template is standard and customisation is minimal.

What if the client wants to change the terms after sending?

Proposify allows editing a proposal after it has been sent — via the API PATCH /proposals/{id}/variables. It is more convenient to implement an “Update Proposal” button in Kommo that updates variables without recreating the document. Version history is preserved.

Two options: pass deal_id as a variable in the proposal ({deal_id}) — it will be in the webhook payload. Or store a {proposal_id -> deal_id} mapping table in your database. The first option is simpler at low volumes.

Summary

  • Proposify REST API: Bearer token, Base URL https://app.proposify.com/api/v2/
  • Create proposal: POST /proposals with document_template_id and variables from Kommo fields
  • Webhook events: proposal.signed -> Won + task, proposal.viewed -> Note, proposal.declined -> task with reason
  • Webhook verification: HMAC-SHA256, X-Proposify-Signature header
  • Idempotency: save proposal_id + event_type to protect against duplicates
  • Typical development time: 1–2 weeks

If you use Proposify and Kommo and want to automate sending proposals from the pipeline — describe your template structure and custom deal fields. Exceltic.dev will configure the variable mapping and signing event handling.

More articles

All →