Почему нативная интеграция не работает
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
- Войдите в FastSpring Dashboard -> Settings -> Webhooks
- Добавьте endpoint URL вашего сервиса, например
https://your-service.example.com/webhooks/fastspring - Задайте HMAC Secret (запомните его - понадобится для валидации)
- Выберите события:
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. Разберём архитектуру за одну встречу.