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
| Parameter | Zuora | Chargebee | Recurly |
|---|---|---|---|
| Revenue recognition | ASC 606/IFRS 15 native | Via integration | Separate module |
| Multi-entity | Yes (multiple legal entities) | Limited | No |
| Usage-based billing | Full support | Yes | Yes |
| ERP integration | SAP, Oracle, NetSuite | NetSuite | No native |
| Target segment | Enterprise (500+ subscriptions) | SMB-Enterprise | Mid-market |
| Price | From $75k/year | From $599/month | From $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/usagemonthly -> 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.