Kommo + FastSpring: приём платежей за SaaS из воронки продаж

Почему нативная интеграция не работает

FastSpring не имеет готовой интеграции с Kommo. В маркетплейсе Kommo вы не найдёте официального виджета FastSpring - и это не случайность. FastSpring позиционирует себя как Merchant of Record, то есть принимает на себя юридическую ответственность за продажу, выставляет инвойс от своего имени, самостоятельно рассчитывает и уплачивает НДС по каждой стране. Для EU SaaS это критично: вам не нужно регистрироваться плательщиком НДС в каждой из 27 стран ЕС.

При этом Kommo - это система управления продажами, в которой живёт контекст сделки: история переговоров, ответственный менеджер, этап воронки. Без интеграции ваш SDR видит сделку в статусе «Переговоры» и не знает, что клиент уже оплатил. Данные разбросаны по двум системам без связи.

Если вы работаете с кастомными интеграциями для Kommo CRM, то знаете: стандартные no-code инструменты здесь не помогут - FastSpring webhooks имеют специфичную структуру и требуют корректной верификации подписи.

Что реализуется - архитектура решения

Архитектура простая: FastSpring отправляет webhook при событии оплаты, Python-сервис валидирует подпись и вызывает Kommo API для обновления сделки.

FastSpring --> Webhook (order.completed / subscription.activated)
    --> Python сервис (HMAC-SHA256 проверка)
        --> Kommo API (обновление сделки / создание note)

Технические детали

FastSpring Webhooks. FastSpring подписывает каждый webhook заголовком X-FS-Signature. Подпись - HMAC-SHA256 от тела запроса с ключом, который вы задаёте в настройках FastSpring (Settings -> Webhooks). Ключевые события:

  • order.completed - разовая оплата или первый платёж подписки
  • subscription.activated - подписка активирована
  • subscription.deactivated - подписка отменена (полезно для даунгрейда в Kommo)
  • subscription.payment.overdue - просрочка платежа

FastSpring API Auth. Для исходящих запросов к FastSpring API используется Basic Auth: API username как логин, пустой пароль (строка ""). Это документированное поведение FastSpring API v2.

Kommo API. Bearer-токен (Long-lived access token или OAuth 2.0). Для обновления сделки - PATCH /api/v4/leads/{id}. Для создания примечания - POST /api/v4/leads/{lead_id}/notes.

Матчинг сделки. FastSpring передаёт email покупателя в поле order.customer.email. По нему ищем контакт в Kommo через GET /api/v4/contacts?query={email}, затем берём связанную сделку.

Пошаговая реализация

Шаг 1. Настройка FastSpring Webhook

  1. Войдите в FastSpring Dashboard -> Settings -> Webhooks
  2. Добавьте endpoint URL вашего сервиса, например https://your-service.example.com/webhooks/fastspring
  3. Задайте HMAC Secret (запомните его - понадобится для валидации)
  4. Выберите события: order.completed, subscription.activated, subscription.deactivated

Шаг 2. Python-сервис для обработки webhook

import hmac
import hashlib
import json
import os
import requests
from flask import Flask, request, abort

app = Flask(__name__)

FASTSPRING_SECRET = os.environ["FASTSPRING_WEBHOOK_SECRET"]
KOMMO_DOMAIN = os.environ["KOMMO_DOMAIN"]  # yourcompany.kommo.com
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]


def verify_fastspring_signature(payload: bytes, signature: str) -> bool:
    """Верифицируем HMAC-SHA256 подпись FastSpring webhook."""
    expected = hmac.new(
        FASTSPRING_SECRET.encode("utf-8"),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


def find_kommo_contact_by_email(email: str) -> dict | None:
    """Ищем контакт в Kommo по email."""
    url = f"https://{KOMMO_DOMAIN}/api/v4/contacts"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    r = requests.get(url, params={"query": email}, headers=headers, timeout=10)
    if not r.ok:
        return None
    data = r.json()
    contacts = data.get("_embedded", {}).get("contacts", [])
    return contacts[0] if contacts else None


def get_contact_leads(contact_id: int) -> list:
    """Получаем открытые сделки контакта."""
    url = f"https://{KOMMO_DOMAIN}/api/v4/contacts/{contact_id}/links"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    r = requests.get(url, headers=headers, timeout=10)
    if not r.ok:
        return []
    links = r.json().get("_embedded", {}).get("links", [])
    return [l["to_entity_id"] for l in links if l.get("to_entity_type") == "leads"]


def update_lead_status(lead_id: int, status_id: int, note_text: str):
    """Обновляем статус сделки и добавляем примечание."""
    headers = {
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type": "application/json",
    }
    # Обновляем этап
    patch_url = f"https://{KOMMO_DOMAIN}/api/v4/leads"
    requests.patch(
        patch_url,
        json=[{"id": lead_id, "status_id": status_id}],
        headers=headers,
        timeout=10,
    )
    # Добавляем note
    notes_url = f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}/notes"
    requests.post(
        notes_url,
        json=[{"note_type": "common", "params": {"text": note_text}}],
        headers=headers,
        timeout=10,
    )


@app.route("/webhooks/fastspring", methods=["POST"])
def fastspring_webhook():
    signature = request.headers.get("X-FS-Signature", "")
    payload = request.get_data()

    if not verify_fastspring_signature(payload, signature):
        abort(403)

    events = request.json.get("events", [])
    for event in events:
        event_type = event.get("type")
        data = event.get("data", {})

        if event_type == "order.completed":
            handle_order_completed(data)
        elif event_type == "subscription.activated":
            handle_subscription_activated(data)

    return {"ok": True}


def handle_order_completed(data: dict):
    """Обрабатываем событие завершения заказа."""
    email = data.get("customer", {}).get("email", "")
    order_id = data.get("id", "")
    total = data.get("total", 0)
    currency = data.get("currency", "USD")

    if not email:
        return

    contact = find_kommo_contact_by_email(email)
    if not contact:
        # Контакт не найден в CRM - создаём новый или логируем
        print(f"Contact not found for email: {email}")
        return

    lead_ids = get_contact_leads(contact["id"])
    if not lead_ids:
        return

    # Берём последнюю активную сделку
    lead_id = lead_ids[0]
    note_text = (
        f"FastSpring: заказ #{order_id} оплачен\n"
        f"Сумма: {total} {currency}\n"
        f"Email: {email}"
    )
    # STATUS_PAID_ID - ID этапа "Оплачено" в вашей воронке Kommo
    STATUS_PAID_ID = int(os.environ.get("KOMMO_STATUS_PAID_ID", 0))
    update_lead_status(lead_id, STATUS_PAID_ID, note_text)


def handle_subscription_activated(data: dict):
    """Обрабатываем активацию подписки."""
    email = data.get("customer", {}).get("email", "")
    subscription_id = data.get("id", "")
    product = data.get("product", {}).get("display", {}).get("en", "")

    contact = find_kommo_contact_by_email(email)
    if not contact:
        return

    lead_ids = get_contact_leads(contact["id"])
    if not lead_ids:
        return

    note_text = (
        f"FastSpring: подписка активирована\n"
        f"ID подписки: {subscription_id}\n"
        f"Продукт: {product}"
    )
    STATUS_WON_ID = int(os.environ.get("KOMMO_STATUS_WON_ID", 0))
    update_lead_status(lead_ids[0], STATUS_WON_ID, note_text)


if __name__ == "__main__":
    app.run(port=5000)

Шаг 3. Идемпотентность

FastSpring гарантирует доставку webhook «at least once». Это значит, что один и тот же order.completed может прийти дважды. Добавьте дедупликацию по order_id:

import redis

r = redis.Redis(host="localhost", port=6379, db=0)

def is_processed(order_id: str) -> bool:
    key = f"fastspring:processed:{order_id}"
    # SETNX + TTL на 24 часа
    return not r.set(key, "1", ex=86400, nx=True)

Шаг 4. Обработка ошибок

Кommo API имеет rate limit 7 запросов/секунду. При ошибке 429 используйте exponential backoff:

import time

def kommo_request_with_retry(method, url, **kwargs):
    for attempt in range(3):
        r = requests.request(method, url, **kwargs)
        if r.status_code == 429:
            time.sleep(2 ** attempt)
            continue
        return r
    raise Exception(f"Kommo API rate limit exceeded after 3 attempts")

Реальный кейс с цифрами

В типовом проекте для EU SaaS с командой из 5-8 SDR интеграция Kommo + FastSpring решает следующее:

До интеграции менеджер узнавал об оплате из уведомления на почте (если не пропускал). Среднее время от оплаты до обновления сделки в CRM - 4-8 часов. При команде 5 SDR и 80 сделках в месяц это 40+ ручных обновлений.

После интеграции сделка переводится в статус «Оплачено» в течение 10-15 секунд после завершения транзакции в FastSpring. SDR видит актуальный статус в реальном времени. Типовая экономия - от 6 до 10 часов менеджерского времени в месяц на команду.

Отдельный выигрыш - EU VAT compliance. FastSpring как MOR самостоятельно добавляет налог в зависимости от страны покупателя. При продажах в Германию добавляется 19% MwSt, во Францию - 20% TVA, в Венгрию - 27% ÁFA. Вам не нужно программировать эти расчёты - FastSpring делает это автоматически и отражает в данных webhook.

Для кого подходит

Интеграция Kommo + FastSpring подходит компаниям, которые:

  • Продают SaaS-продукты в EU и не хотят самостоятельно разбираться с VAT по каждой стране
  • Используют Kommo как основную CRM и хотят видеть статус оплаты прямо в карточке сделки
  • Работают с подписочной моделью (месячные, годовые планы) и хотят автоматически менять этап сделки при активации/отмене подписки
  • Имеют команду SDR от 3 человек, которым важна актуальность данных

Если вы уже используете другие платёжные интеграции - например, Kommo + Stripe - то FastSpring занимает другую нишу: Stripe - это платёжный процессор, FastSpring - полноценный Merchant of Record с налоговой ответственностью.

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

Можно ли использовать Zapier вместо кастомного кода? Technically yes, но FastSpring webhooks требуют верификации HMAC-SHA256 подписи. Zapier не позволяет выполнять кастомную верификацию подписи из коробки. Без неё любой может отправить поддельный webhook на ваш endpoint. Для продакшн-использования рекомендуется кастомный сервис.

Что происходит, если email в FastSpring не совпадает с email в Kommo? Это типичная ситуация: клиент мог указать рабочий email при регистрации в Kommo и личный при оплате. Рекомендуется добавить запасной матчинг по домену компании: если @company.com не найден точно, искать все контакты с тем же доменом и выбирать по дополнительным критериям (имя, компания).

FastSpring поддерживает несколько продуктов. Как понять, к какой сделке привязать оплату? В поле order.items каждого события FastSpring есть product.sku. Добавьте маппинг SKU -> ID воронки в Kommo в конфигурационный файл. Так оплата продукта «Pro» пойдёт в воронку «SaaS Pro», а «Enterprise» - в «SaaS Enterprise».

Нужна ли поддержка subscription.deactivated? Рекомендуется обрабатывать. При отмене подписки можно автоматически создавать задачу «Winback call» для SDR в Kommo - это возможность для retention.

Если вам нужна интеграция Kommo с FastSpring - опишите ваш стек и сценарий команде Exceltic.dev. Разберём архитектуру за одну встречу.

Ещё статьи

Все →