Discuss your task

Search

Start typing to search articles, cases, and services.

navigate Esc close

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
Get in touch your way
WhatsApp Telegram

Before you go: bonus months of Kommo

The implementation estimate is free: describe your sales process in a few words and we propose a solution, a plan and a quote with timelines.

We are an official Kommo partner in Europe: pay for the subscription through us and get bonus months.