Kommo + Zuora: enterprise subscriptions from won deals without manual entry

Zuora is the enterprise subscription management market leader: a platform for companies with 500+ subscribers, revenue recognition requirements under ASC 606/IFRS 15, multi-entity billing, and complex usage-based pricing. Unlike Chargebee or Recurly, Zuora targets the enterprise segment - public companies, financial-regulatory requirements, and ERP integrations (SAP, Oracle). Without integration with Kommo: a manager closes a deal -> manually creates an Account and Subscription in Zuora. 20-40 minutes. 3-5% errors in pricing plans. With the integration: Won -> account and subscription created 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, NetSuiteNetSuiteNo native
Target segmentEnterprise (500+ subscriptions)SMB-EnterpriseMid-market
PriceFrom $75k/yearFrom $599/monthFrom $149/month

Zuora is chosen by public companies, SaaS with multi-product billing, and Revenue Ops teams that need a single system 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 right approach: a custom webhook service that listens for 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 client
    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 HTTP callouts (webhooks) on billing events. Configuration: 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 canceled")

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

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

    return "", 200

Usage-based billing: submitting consumption metrics

Zuora supports usage-based billing - pricing by consumption (API calls, GB of data, users). At Won we record a baseline; an ETL job then submits usage metrics:

def submit_usage(subscription_number: str, unit_type: str,
                 quantity: float, start_date: str, end_date: str):
    # unit_type - matches Unit of Measure 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 case

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

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

Who this is 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 ERP

Frequently asked questions

Zuora and NetSuite - how are they connected when integrating with Kommo?

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

How does revenue recognition work with Kommo + Zuora?

When a Subscription is created via API, Zuora automatically generates a Revenue Schedule according to 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 the correct waterfall without manual adjustments.

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

Zuora REST API (rest.zuora.com/v1) is current and supported. Zuora API Legacy (SOAP/XML) is deprecated; some enterprise clients still use it for historical reasons, but new integrations should be built on REST only. For creating Account, Subscription, and Usage - REST is sufficient.

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

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 changes in a Kommo custom field -> webhook -> Zuora plan change -> Note in 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 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 pricing plan structure and revenue recognition requirements. Exceltic.dev will set up the integration with usage billing and renewal notification support.

More articles

All →