Kommo + Zuora: enterprise-подписки из выигранных сделок без ручного ввода
Zuora — лидер рынка enterprise subscription management: платформа для компаний с 500+ подписчиками, требованиями к revenue recognition по ASC 606/IFRS 15, multi-entity биллингу и сложным usage-based тарифам. В отличие от Chargebee или Recurly, Zuora ориентирован на enterprise-сегмент — публичные компании, финансово-регуляторные требования, интеграция с ERP (SAP, Oracle). Без интеграции с Kommo: менеджер закрывает сделку -> вручную создаёт Account и Subscription в Zuora. 20–40 минут. 3–5% ошибок в тарифных планах. С интеграцией: Won -> аккаунт и подписка за секунды.
Zuora vs Chargebee vs Recurly для enterprise
| Параметр | Zuora | Chargebee | Recurly |
|---|---|---|---|
| Revenue recognition | ASC 606/IFRS 15 нативно | Через интеграцию | Отдельный модуль |
| Multi-entity | Да (несколько юр. лиц) | Ограниченно | Нет |
| Usage-based billing | Полная поддержка | Да | Да |
| ERP интеграция | SAP, Oracle, NetSuite | NetSuite | Нет нативно |
| Целевой сегмент | Enterprise (500+ подписок) | SMB–Enterprise | Mid-market |
| Цена | От $75k/год | От $599/мес | От $149/мес |
Zuora выбирают публичные компании, SaaS с multi-product биллингом и командами Revenue Ops которым нужна единая система от CPQ до признания выручки.
Архитектура интеграции Kommo -> Zuora
Kommo Won -> Python webhook handler -> Zuora REST API
Zuora billing events -> HTTP callout -> Kommo Notes + Tasks
Zuora не имеет прямого маркетплейс-коннектора для Kommo. Правильный путь: собственный webhook-сервис, который слушает события Kommo и вызывает Zuora REST API.
Аутентификация: Zuora OAuth 2.0
Zuora использует OAuth 2.0 Client Credentials. Токен получается один раз и кешируется:
import requests
import time
ZUORA_CLIENT_ID = "your_client_id"
ZUORA_CLIENT_SECRET = "your_client_secret"
ZUORA_BASE_URL = "https://rest.zuora.com/v1"
_token_cache = {"token": None, "expires_at": 0}
def get_zuora_token() -> str:
if _token_cache["token"] and time.time() < _token_cache["expires_at"] - 60:
return _token_cache["token"]
resp = requests.post(
"https://rest.zuora.com/oauth/token",
data={
"client_id": ZUORA_CLIENT_ID,
"client_secret": ZUORA_CLIENT_SECRET,
"grant_type": "client_credentials",
},
)
resp.raise_for_status()
data = resp.json()
_token_cache["token"] = data["access_token"]
_token_cache["expires_at"] = time.time() + data["expires_in"]
return _token_cache["token"]
def zuora_headers() -> dict:
return {
"Authorization": f"Bearer {get_zuora_token()}",
"Content-Type": "application/json",
}
Создание аккаунта и подписки в Zuora при Won
PLAN_MAP = {
"starter": "2c92c0f96d7ee1f5016d879c15cd0987",
"growth": "2c92c0f96d7ee1f5016d879c17ab0989",
"enterprise": "2c92c0f96d7ee1f5016d879c19cd098b",
}
def create_zuora_account(contact: dict, lead: dict) -> dict:
# Zuora Account = billing entity, создаётся один раз на клиента
name = contact.get("name", "")
email = get_contact_email(contact)
payload = {
"name": name,
"currency": "USD",
"billToContact": {
"firstName": name.split()[0] if name else "",
"lastName": " ".join(name.split()[1:]) if len(name.split()) > 1 else "",
"workEmail": email,
"country": "US",
},
"paymentTerm": "Net 30",
"crmId": str(lead["id"]),
"notes": f"Kommo deal ID: {lead['id']}",
}
resp = requests.post(
f"{ZUORA_BASE_URL}/accounts",
headers=zuora_headers(),
json=payload,
)
resp.raise_for_status()
return resp.json()
def create_zuora_subscription(account_key: str, rate_plan_id: str,
start_date: str = None) -> dict:
# start_date format: "2026-06-01"
import datetime
if not start_date:
start_date = datetime.date.today().isoformat()
payload = {
"accountKey": account_key,
"contractEffectiveDate": start_date,
"terms": {
"initialTerm": {
"period": 12,
"periodType": "Month",
"termType": "TERMED",
},
"autoRenew": True,
},
"subscribeToRatePlans": [
{
"productRatePlanId": rate_plan_id,
}
],
}
resp = requests.post(
f"{ZUORA_BASE_URL}/subscriptions",
headers=zuora_headers(),
json=payload,
)
resp.raise_for_status()
return resp.json()
def on_kommo_deal_won(lead: dict, contact: dict):
plan_field = get_custom_field(lead, PLAN_FIELD_ID) or "starter"
rate_plan_id = PLAN_MAP.get(plan_field.lower(), PLAN_MAP["starter"])
account_resp = create_zuora_account(contact, lead)
account_key = account_resp.get("accountNumber") or account_resp.get("id")
sub_resp = create_zuora_subscription(account_key, rate_plan_id)
sub_number = sub_resp.get("subscriptionNumber")
save_to_kommo_deal(lead["id"], {
"zuora_account_key": account_key,
"zuora_subscription_number": sub_number,
})
create_kommo_note(
lead["id"],
f"Zuora: аккаунт {account_key}, подписка {sub_number} ({plan_field}) активна",
)
Billing events: Zuora -> Kommo Notes
Zuora отправляет HTTP callout (webhook) при биллинговых событиях. Настройка: Zuora -> Settings -> Notifications -> Add Notification -> HTTP Callout.
@app.route("/webhooks/zuora", methods=["POST"])
def zuora_webhook():
payload = request.json
event_id = payload.get("eventType", "")
crm_id = payload.get("Account", {}).get("crmId", "")
lead_id = find_kommo_deal_by_custom_field("zuora_crm_id", crm_id)
if not lead_id:
return "", 200
if event_id == "PaymentProcessed":
amount = payload.get("Payment", {}).get("amount", 0)
create_kommo_note(lead_id,
f"Zuora: платёж обработан - ${amount:.2f}")
elif event_id == "PaymentProcessingError":
create_kommo_note(lead_id, "Zuora: ошибка оплаты - нужна проверка")
create_kommo_task(lead_id,
"Zuora: связаться с клиентом - платёж не прошёл")
elif event_id == "SubscriptionCanceled":
create_kommo_note(lead_id, "Zuora: подписка отменена")
elif event_id == "SubscriptionRenewed":
create_kommo_note(lead_id, "Zuora: подписка продлена автоматически")
elif event_id == "ContractRenewalReminder":
create_kommo_task(lead_id,
"Zuora: контракт истекает через 30 дней - обсудить renewal")
return "", 200
Usage-based billing: передача метрик потребления
Zuora поддерживает usage-based billing — тарификацию по потреблению (API вызовы, GB данных, пользователи). При Won фиксируем baseline, далее ETL передаёт usage метрики:
def submit_usage(subscription_number: str, unit_type: str,
quantity: float, start_date: str, end_date: str):
# unit_type - совпадает с Unit of Measure в Zuora продукте
payload = {
"subscriptionNumber": subscription_number,
"unitOfMeasure": unit_type,
"quantity": quantity,
"startDateTime": f"{start_date}T00:00:00",
"endDateTime": f"{end_date}T23:59:59",
}
resp = requests.post(
f"{ZUORA_BASE_URL}/usage",
headers=zuora_headers(),
json=payload,
)
resp.raise_for_status()
return resp.json()
Этот endpoint вызывается ежемесячно из cron-задачи — Zuora рассчитывает invoice автоматически.
Реальный кейс
B2B SaaS (US, публичная компания, 1200+ подписчиков, Kommo + Zuora + NetSuite):
- До: Won -> ручное создание в Zuora (25 мин) -> ручная синхронизация с NetSuite для revenue recognition. 8% сделок содержали ошибки в плане -> ручные кредит-ноты.
- После: Won -> Python webhook -> Zuora Account + Subscription за 8 секунд. CRM ID сохраняется в Zuora -> двустороннее сопоставление. Ошибки в плане: 0 за 10 месяцев.
- Дополнительно:
ContractRenewalReminder(30 дней до renewal) -> задача в Kommo -> account manager инициирует upsell. NRR вырос на 11% — renewal conversations начались вовремя.
Для кого актуально
- SaaS-компании с требованиями к ASC 606 revenue recognition (публичные, pre-IPO)
- Multi-entity бизнесы: несколько юр. лиц, несколько валют, единая подписка
- Компании с usage-based billing где объём потребления влияет на invoice
- Enterprise-команды Revenue Ops где Zuora уже интегрирован с ERP
Часто задаваемые вопросы
Zuora и NetSuite — как они связаны при интеграции с Kommo?
Zuora -> NetSuite интеграция (Zuora for NetSuite connector) синхронизирует Invoices, Payments, Revenue Schedules. Kommo-интеграция работает на уровне Zuora — создание Account и Subscription. NetSuite получает данные уже из Zuora автоматически. Kommo -> Zuora -> NetSuite: трёхзвенная цепочка без ручного ввода на каждом шаге.
Как работает revenue recognition в связке Kommo + Zuora?
При создании Subscription через API Zuora автоматически генерирует Revenue Schedule согласно настроенным правилам ASC 606. Дата признания выручки привязана к delivery-событиям (provision date). Kommo -> Won -> контрактная дата -> Zuora фиксирует её как contractEffectiveDate. Revenue Ops видят корректный waterfall без ручных корректировок.
Zuora REST API v1 vs Zuora API Legacy — что использовать?
Zuora REST API (rest.zuora.com/v1) — текущий, поддерживаемый. Zuora API Legacy (SOAP/XML) — устаревший, некоторые enterprise-клиенты ещё используют его для исторических причин, но новые интеграции строить только на REST. Для создания Account, Subscription, Usage — REST достаточно.
Можно ли изменить тарифный план через API без потери данных?
Да. POST /v1/subscriptions/{subscriptionKey}/upgrade или PUT /v1/subscriptions/{subscriptionKey} с новым ratePlanId. Zuora автоматически рассчитывает proration на остаток периода и корректирует следующий invoice. При смене тарифа в Kommo кастомном поле -> webhook -> Zuora plan change -> Note в карточку.
Итого
- Аутентификация: OAuth 2.0 Client Credentials, токен кешировать (TTL ~1 час)
- Поток: Won -> create Account (crmId = Kommo deal ID) -> create Subscription (ratePlanId)
- Billing events через Zuora HTTP Callout -> Kommo Notes + Tasks
- Usage-based:
POST /v1/usageежемесячно -> Zuora генерирует invoice автоматически - Revenue recognition: дата контракта из Won передаётся как
contractEffectiveDate
Если вы используете Zuora и Kommo и хотите автоматизировать создание подписок при Won — опишите структуру тарифных планов и требования к revenue recognition. Exceltic.dev настроит интеграцию с поддержкой usage-billing и renewal-уведомлений.