Kommo + Zuora: enterprise subscriptions from won deals with no manual entry

Zuora is the enterprise subscription management market leader: a platform for companies with 500+ subscribers, ASC 606/IFRS 15 revenue recognition requirements, multi-entity billing, and complex usage-based pricing tiers. Unlike Chargebee or Recurly, Zuora is built for the enterprise segment — public companies, financial regulatory compliance, ERP integration (SAP, Oracle). Without a Kommo integration: the sales rep closes a deal -> manually creates an Account and Subscription in Zuora. 20–40 minutes. 3–5% error rate in plan selection. With the integration: Won -> account and subscription provisioned in seconds.

Zuora vs Chargebee vs Recurly for enterprise

ParameterZuoraChargebeeRecurly
Revenue recognitionASC 606/IFRS 15 nativeVia integrationSeparate module
Multi-entityYes (multiple legal entities)LimitedNo
Usage-based billingFull supportYesYes
ERP integrationSAP, Oracle, NetSuiteNetSuiteNone native
Target segmentEnterprise (500+ subscriptions)SMB–EnterpriseMid-market
PriceFrom $75k/yearFrom $599/monthFrom $149/month

Zuora is chosen by public companies, SaaS businesses with multi-product billing, and Revenue Ops teams that need a single system covering everything from CPQ to revenue recognition.

Integration architecture: Kommo -> Zuora

Kommo Won  ->  Python webhook handler  ->  Zuora REST API
Zuora billing events  ->  HTTP callout  ->  Kommo Notes + Tasks

Zuora has no direct marketplace connector for Kommo. The correct approach: a custom webhook service that listens to Kommo events and calls the Zuora REST API.

Authentication: Zuora OAuth 2.0

Zuora uses OAuth 2.0 Client Credentials. The token is obtained once and cached:

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",
    }

Creating an Account and Subscription in Zuora on Won

PLAN_MAP = {
    "starter":    "2c92c0f96d7ee1f5016d879c15cd0987",
    "growth":     "2c92c0f96d7ee1f5016d879c17ab0989",
    "enterprise": "2c92c0f96d7ee1f5016d879c19cd098b",
}

def create_zuora_account(contact: dict, lead: dict) -> dict:
    # Zuora Account = billing entity, created once per customer
    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 {account_key}, subscription {sub_number} ({plan_field}) active",
    )

Billing events: Zuora -> Kommo Notes

Zuora sends an HTTP callout (webhook) for billing events. Setup: 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: payment processed - ${amount:.2f}")

    elif event_id == "PaymentProcessingError":
        create_kommo_note(lead_id, "Zuora: payment error - review required")
        create_kommo_task(lead_id,
            "Zuora: contact client - payment failed")

    elif event_id == "SubscriptionCanceled":
        create_kommo_note(lead_id, "Zuora: subscription cancelled")

    elif event_id == "SubscriptionRenewed":
        create_kommo_note(lead_id, "Zuora: subscription renewed automatically")

    elif event_id == "ContractRenewalReminder":
        create_kommo_task(lead_id,
            "Zuora: contract expires in 30 days - discuss renewal")

    return "", 200

Usage-based billing: passing consumption metrics

Zuora supports usage-based billing — charging based on consumption (API calls, GB of data, users). Upon Won, a baseline is recorded; an ETL job then passes usage metrics:

def submit_usage(subscription_number: str, unit_type: str,
                 quantity: float, start_date: str, end_date: str):
    # unit_type must match the Unit of Measure defined in the Zuora product
    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()

This endpoint is called monthly from a cron job — Zuora calculates the invoice automatically.

Real-world case

B2B SaaS (US, public company, 1,200+ subscribers, Kommo + Zuora + NetSuite):

  • Before: Won -> manual creation in Zuora (25 min) -> manual sync with NetSuite for revenue recognition. 8% of deals contained plan errors -> manual credit notes required.
  • After: Won -> Python webhook -> Zuora Account + Subscription in 8 seconds. CRM ID stored in Zuora -> bidirectional mapping. Plan errors: 0 over 10 months.
  • Additional: ContractRenewalReminder (30 days before renewal) -> task in Kommo -> account manager initiates upsell conversation. NRR increased by 11% — renewal conversations started on time.

Who this is relevant for

  • SaaS companies with ASC 606 revenue recognition requirements (public, pre-IPO)
  • Multi-entity businesses: multiple legal entities, multiple currencies, unified subscription
  • Companies with usage-based billing where consumption volume affects the invoice
  • Enterprise Revenue Ops teams where Zuora is already integrated with an ERP

Frequently asked questions

How are Zuora and NetSuite connected when integrating with Kommo?

The Zuora -> NetSuite integration (Zuora for NetSuite connector) syncs Invoices, Payments, and Revenue Schedules. The Kommo integration operates at the Zuora level — creating the Account and Subscription. NetSuite receives data from Zuora automatically. Kommo -> Zuora -> NetSuite: a three-tier chain with no manual entry at any step.

How does revenue recognition work in the Kommo + Zuora combination?

When a Subscription is created via API, Zuora automatically generates a Revenue Schedule according to the configured ASC 606 rules. The revenue recognition date is tied to delivery events (provision date). Kommo -> Won -> contract date -> Zuora records it as contractEffectiveDate. Revenue Ops sees a correct waterfall without manual adjustments.

Zuora REST API v1 vs Zuora API Legacy — which to use?

Zuora REST API (rest.zuora.com/v1) — current, supported. Zuora API Legacy (SOAP/XML) — deprecated; some enterprise clients still use it for historical reasons, but all new integrations should use REST only. For creating Accounts, Subscriptions, and Usage — REST is sufficient.

Can a pricing plan be changed via API without losing data?

Yes. POST /v1/subscriptions/{subscriptionKey}/upgrade or PUT /v1/subscriptions/{subscriptionKey} with a new ratePlanId. Zuora automatically calculates proration for the remaining period and adjusts the next invoice. When the plan is changed in a Kommo custom field -> webhook -> Zuora plan change -> Note added to the deal card.

Summary

  • Authentication: OAuth 2.0 Client Credentials, cache the token (TTL ~1 hour)
  • Flow: Won -> create Account (crmId = Kommo deal ID) -> create Subscription (ratePlanId)
  • Billing events via Zuora HTTP Callout -> Kommo Notes + Tasks
  • Usage-based: POST /v1/usage monthly -> Zuora generates the invoice automatically
  • Revenue recognition: contract date from Won passed as contractEffectiveDate

If you use Zuora and Kommo and want to automate subscription creation on Won — describe your rate plan structure and revenue recognition requirements. Exceltic.dev will configure the integration with usage-billing support and renewal notifications.

More articles

All →