Kommo + Proposify: коммерческие предложения из карточки сделки

Kommo + Proposify: коммерческие предложения из карточки сделки

Proposify — платформа для создания интерактивных коммерческих предложений с электронной подписью, трекингом просмотров и REST API. Без интеграции с Kommo менеджер после демо вручную открывает Proposify, копирует имя, компанию, сумму из CRM, выбирает шаблон. С интеграцией нажатие кнопки в карточке сделки создаёт пропозал с уже заполненными переменными, отправляет клиенту — и при подписании автоматически переводит сделку в Won.

Почему нативных инструментов недостаточно

Proposify интегрируется с HubSpot, Pipedrive, Salesforce и Zoho нативно — через встроенные коннекторы в интерфейсе. Kommo в этот список не входит. Zapier предлагает базовый триггер «proposal signed», но не умеет передавать кастомные поля из Kommo как переменные пропозала и не поддерживает двустороннюю синхронизацию статусов.

Проблема глубже в архитектуре: переменные в шаблоне Proposify ({{client_name}}, {{deal_amount}}, {{plan}}) нужно заполнять на этапе создания документа — из полей конкретной сделки Kommo, включая кастомные поля. Это требует прямого вызова Proposify API с параметрами из Kommo API. Ни один no-code коннектор это не реализует корректно.

Proposify — сервис интерактивных пропозалов, где клиент может просмотреть предложение онлайн, оставить комментарий и подписать электронной подписью. Каждое действие клиента (открыл, просмотрел секцию, подписал) генерирует webhook-событие.

Что даёт связка Kommo + Proposify

Без интеграции:
— Менеджер после демо открывает Proposify вручную, копирует данные из Kommo
— Среднее время от демо до отправки пропозала — 20–40 минут
— При подписании Proposify не уведомляет Kommo — сделка обновляется вручную или забывается
— Sales Director не видит в CRM, когда клиент открыл и читал предложение

С интеграцией:
— Кнопка в карточке сделки -> пропозал создан и отправлен за 30 секунд
— Имя контакта, компания, тариф, сумма подставляются автоматически из полей сделки
proposal.viewed -> Note в Kommo: «Клиент открыл пропозал — {дата}»
proposal.signed -> сделка переходит в Won, задача бухгалтеру на инвойс
proposal.declined -> задача менеджеру + кастомное поле с причиной

Что синхронизируется

Kommo -> Proposify:
— Имя и email контакта -> переменные {{client_name}}, {{client_email}}
— Название компании -> {{company_name}}
— Сумма сделки -> {{deal_amount}}
— Тариф из кастомного поля -> {{plan}}, {{billing_period}}
— ID сделки -> custom attribute для обратной трассировки

Proposify -> Kommo:
proposal.viewed -> Note с временем просмотра
proposal.signed -> Won + Note «Пропозал подписан» + задача на инвойс
proposal.declined -> кастомное поле proposal_status = declined + задача менеджеру
— URL пропозала -> кастомное поле в сделке (для быстрого доступа)

Архитектура

Kommo: менеджер нажимает кнопку «Создать пропозал» (кастомная виджет-кнопка)
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> имя, email, компания, тариф, сумма
  2. Proposify: POST /proposals
     -> template_id + variables (client_name, deal_amount, plan...)
     -> получить proposal_id + proposal_url
  3. Kommo: PATCH /leads/{id}
     -> кастомное поле proposal_url = ссылка на пропозал
     -> кастомное поле proposal_status = sent
  4. Kommo: POST /leads/{id}/notes
     -> «Пропозал отправлен клиенту: {proposal_url}»

Proposify Webhook: proposal.viewed
  ↓ Backend
  1. Из payload: proposal_id, viewed_at, contact_email
  2. Найти deal_id по proposal_id (сохранён в шаге 3 выше)
  3. Kommo: POST /leads/{deal_id}/notes
     -> «Клиент открыл пропозал: {viewed_at}»

Proposify Webhook: proposal.signed
  ↓ Backend
  1. Из payload: proposal_id, signed_at, signer_name
  2. Найти deal_id
  3. Kommo: PATCH /leads/{deal_id}
     -> pipeline_id + status_id -> Won
     -> proposal_status = signed
  4. Kommo: POST /tasks
     -> задача: «Выставить инвойс - пропозал подписан {signed_at}»

Proposify REST API: ключевые запросы

Base URL: https://app.proposify.com/api/v2/. Аутентификация: Bearer token в заголовке Authorization: Bearer {API_KEY}.

Создать пропозал из шаблона:

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']
    }

Обработка 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' и т.д.
    proposal = payload.get('proposal', {})
    proposal_id = proposal.get('id')

    deal_id = get_deal_id_by_proposal(proposal_id)  # из вашей БД или кастомного поля
    if not deal_id:
        return '', 200  # пропозал не от Kommo - игнорируем

    if event_type == 'proposal.signed':
        signed_at = proposal.get('signed_at')
        # Переводим сделку в Won
        move_kommo_deal_to_won(deal_id)
        # Создаём Note
        create_kommo_note(deal_id,
            f'Пропозал Proposify подписан {signed_at}. '
            f'Подписант: {proposal.get("signer_name")}')
        # Задача на инвойс
        create_kommo_task(deal_id, 'Пропозал подписан - выставить инвойс')

    elif event_type == 'proposal.viewed':
        viewed_at = proposal.get('viewed_at')
        create_kommo_note(deal_id,
            f'Клиент открыл пропозал Proposify: {viewed_at}')

    elif event_type == 'proposal.declined':
        reason = proposal.get('decline_reason', 'не указана')
        update_kommo_deal(deal_id, {'proposal_status': 'declined'})
        create_kommo_task(deal_id,
            f'Клиент отклонил пропозал. Причина: {reason}')

    return '', 200

Верификация: Proposify подписывает каждый webhook через HMAC-SHA256, передавая подпись в заголовке X-Proposify-Signature. Всегда проверяйте подпись перед обработкой — это защита от поддельных запросов.

Идемпотентность: Proposify может повторить webhook при таймауте. Сохраняйте обработанные proposal_id + event_type и пропускайте дубликаты.

Маппинг переменных Kommo -> Proposify

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 кастомного поля «Тариф»
    PERIOD_FIELD_ID = 123457  # ID поля «Период биллинга»

    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'))
    }

Переменные в шаблоне Proposify создаются в редакторе: вставьте {{название}} в текст — и они станут доступны через API. Имена переменных должны совпадать с ключами в словаре variables.

Реальный кейс

B2B SaaS (DACH-регион, 25–30 новых квалифицированных лидов в месяц, Kommo + Proposify + Stripe):

  • До: менеджер после демо тратил 30–40 минут на подготовку пропозала. Ошибки в суммах и тарифах из-за ручного копирования. При подписании Kommo обновлялся с задержкой 1–2 дня.
  • После: кнопка «Отправить пропозал» в карточке -> пропозал у клиента за 30 секунд. Данные подставляются из полей сделки — ошибок нет. При подписании Kommo автоматически уходит в Won, бухгалтер получает задачу на инвойс.
  • Дополнительно: Sales Director видит в таймлайне сделки, когда клиент открыл пропозал — и сколько раз. Это меняет тактику follow-up: звонить через час после первого просмотра эффективнее, чем на следующий день.

Аналогичный паттерн для PandaDoc и DocuSign — там те же принципы webhook + переменные шаблона.

Для кого актуально

  • B2B-компании, которые отправляют 15+ пропозалов в месяц
  • Используют Proposify как основной инструмент для коммерческих предложений
  • Хотят видеть статус предложения прямо в Kommo без перехода в Proposify
  • Процесс: демо -> пропозал -> подписание -> Won должен быть автоматическим

Часто задаваемые вопросы

Proposify API — это публичный или enterprise-only API?

Proposify публичный API доступен для пользователей на тарифе Business и выше. API Key создаётся в настройках аккаунта: Settings -> Integrations -> API. Аутентификация через Bearer token в заголовке запроса.

Как настроить webhook в Proposify?

Settings -> Integrations -> Webhooks -> Add Webhook URL. Выбрать события: proposal.viewed, proposal.signed, proposal.declined. Proposify подписывает payload через HMAC-SHA256 — секретный ключ задаётся там же и используется для верификации запросов на вашем сервере.

Можно ли создавать пропозалы без нажатия кнопки — автоматически при переходе на этап?

Да — через Kommo webhook на смену статуса. При переходе сделки на этап «Коммерческое предложение» backend автоматически создаёт и отправляет пропозал. Это работает если шаблон стандартный, а кастомизация минимальна.

Что если клиент хочет изменить условия после отправки?

Proposify позволяет редактировать пропозал после отправки — через API PATCH /proposals/{id}/variables. Удобнее реализовать кнопку «Обновить пропозал» в Kommo, которая обновляет переменные без пересоздания документа. История версий сохраняется.

Как связать proposal_id с deal_id для обратной трассировки?

Два варианта: передавать deal_id как переменную в пропозале ({deal_id}) — она будет в webhook payload. Или хранить в таблице {proposal_id -> deal_id} в вашей БД. Первый вариант проще при небольших объёмах.

Итого

  • Proposify REST API: Bearer token, Base URL https://app.proposify.com/api/v2/
  • Создание пропозала: POST /proposals с document_template_id и variables из полей Kommo
  • Webhook события: proposal.signed -> Won + задача, proposal.viewed -> Note, proposal.declined -> задача с причиной
  • Верификация webhook: HMAC-SHA256, заголовок X-Proposify-Signature
  • Идемпотентность: сохранять proposal_id + event_type для защиты от дублей
  • Типовой срок разработки — 1–2 недели

Если вы используете Proposify и Kommo и хотите автоматизировать отправку пропозалов из воронки — опишите вашу структуру шаблонов и кастомные поля сделки. Exceltic.dev настроит маппинг переменных и обработку событий подписания.

Ещё статьи

Все →