Kommo + Zuora: enterprise-подписки из выигранных сделок без ручного ввода

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

ПараметрZuoraChargebeeRecurly
Revenue recognitionASC 606/IFRS 15 нативноЧерез интеграциюОтдельный модуль
Multi-entityДа (несколько юр. лиц)ОграниченноНет
Usage-based billingПолная поддержкаДаДа
ERP интеграцияSAP, Oracle, NetSuiteNetSuiteНет нативно
Целевой сегментEnterprise (500+ подписок)SMB–EnterpriseMid-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-уведомлений.

Ещё статьи

Все →