Recurly is an enterprise subscription management platform for SaaS and media businesses: automatic dunning management, proration on plan changes, tax compliance (Avalara integration), multi-currency, and revenue recognition. Unlike Chargebee or Stripe Billing, Recurly specialises in complex subscription models with multiple plans, add-ons, and usage-based billing. Without Kommo integration, Won in the CRM and creating a subscription in Recurly are two manual steps. With the integration, Won -> account + subscription in seconds.
Recurly vs Chargebee vs Stripe Billing
| Parameter | Recurly | Chargebee | Stripe Billing |
|---|---|---|---|
| Dunning management | Automatic, configurable | Automatic | Via Smart Retries |
| Proration | Built-in | Built-in | Partial |
| Tax compliance | Avalara native | Avalara/TaxJar | Stripe Tax |
| Usage-based billing | Yes | Yes | Yes |
| Revenue recognition | ASC 606 built-in | Via integration | Separate module |
| Best for | Enterprise SaaS, media | SMB–Enterprise SaaS | Developers, marketplace |
Recurly is chosen by companies with 500+ subscribers, complex pricing, and revenue recognition requirements (public companies, enterprise procurement).
What Gets Synchronised
Kommo -> Recurly: — Won -> create Account with contact data — Won -> create Subscription on the required plan — Plan change -> update Subscription (proration is automatic) — Client churn -> cancel Subscription
Recurly -> Kommo:
— invoice.paid -> Note: “Recurly: payment received, $amount”
— invoice.past_due -> Note + task: “Invoice overdue — contact the client”
— subscription.canceled -> Note: “Subscription cancelled”
— subscription.reactivated -> Note: “Subscription reactivated”
Recurly API: Key Requests
Base URL: https://v3.recurly.com.
Authentication: Basic Auth — API key as username, empty password (or Bearer in header).
API Version: header Accept: application/vnd.recurly.v2021-02-25.
import requests
from requests.auth import HTTPBasicAuth
RECURLY_API_KEY = "your_api_key" # from Recurly Settings -> API Credentials
RECURLY_BASE_URL = "https://v3.recurly.com"
HEADERS = {
"Accept": "application/vnd.recurly.v2021-02-25",
"Content-Type": "application/json",
}
AUTH = HTTPBasicAuth(RECURLY_API_KEY, "")
def create_account(code: str, email: str, first_name: str,
last_name: str, company: str = "") -> dict:
# code - unique identifier (usually email or CRM deal ID)
payload = {
"code": code,
"email": email,
"first_name": first_name,
"last_name": last_name,
"company": company,
}
resp = requests.post(
f"{RECURLY_BASE_URL}/accounts",
headers=HEADERS, auth=AUTH, json=payload
)
resp.raise_for_status()
return resp.json()
def create_subscription(account_code: str, plan_code: str,
currency: str = "USD") -> dict:
# plan_code - plan code from Recurly Plans
payload = {
"account": {"code": account_code},
"plan_code": plan_code,
"currency": currency,
}
resp = requests.post(
f"{RECURLY_BASE_URL}/subscriptions",
headers=HEADERS, auth=AUTH, json=payload
)
resp.raise_for_status()
return resp.json()
def change_subscription_plan(subscription_uuid: str, plan_code: str,
timeframe: str = "now") -> dict:
# timeframe: "now" | "renewal" - immediately with proration or at next renewal
payload = {
"plan_code": plan_code,
"timeframe": timeframe,
}
resp = requests.put(
f"{RECURLY_BASE_URL}/subscriptions/{subscription_uuid}",
headers=HEADERS, auth=AUTH, json=payload
)
resp.raise_for_status()
return resp.json()
def cancel_subscription(subscription_uuid: str) -> dict:
resp = requests.put(
f"{RECURLY_BASE_URL}/subscriptions/{subscription_uuid}/cancel",
headers=HEADERS, auth=AUTH
)
resp.raise_for_status()
return resp.json()
# Kommo plan -> Recurly plan code mapping
PLAN_MAP = {
"starter": "plan_starter_monthly",
"growth": "plan_growth_monthly",
"scale": "plan_scale_monthly",
}
def on_deal_won(lead: dict, contact: dict):
email = get_contact_email(contact)
name = contact["name"].split()
first_name = name[0] if name else ""
last_name = " ".join(name[1:]) if len(name) > 1 else ""
company = get_custom_field(lead, COMPANY_FIELD_ID) or ""
plan = get_custom_field(lead, PLAN_FIELD_ID) or "starter"
account_code = f"kommo_{lead['id']}"
account = create_account(
code=account_code,
email=email,
first_name=first_name,
last_name=last_name,
company=company,
)
plan_code = PLAN_MAP.get(plan.lower(), "plan_starter_monthly")
sub = create_subscription(account_code=account_code, plan_code=plan_code)
update_kommo_deal(lead["id"], {
"recurly_account_code": account_code,
"recurly_subscription_uuid": sub["uuid"],
})
create_kommo_note(lead["id"],
f"Recurly: account created, subscription {plan_code} active (UUID: {sub['uuid']})")
def on_plan_upgrade(lead: dict, contact: dict, old_plan: str, new_plan: str):
sub_uuid = get_deal_field(lead["id"], "recurly_subscription_uuid")
if not sub_uuid:
return
new_plan_code = PLAN_MAP.get(new_plan.lower(), "plan_starter_monthly")
change_subscription_plan(sub_uuid, new_plan_code, timeframe="now")
create_kommo_note(lead["id"],
f"Recurly: plan changed to {new_plan_code} (proration automatic)")
Handling Recurly Webhook:
import hmac, hashlib
RECURLY_WEBHOOK_KEY = "your_webhook_signing_key"
@app.route("/webhooks/recurly", methods=["POST"])
def recurly_webhook():
# Recurly signs webhooks via HMAC-SHA256
sig = request.headers.get("Recurly-Signature", "")
timestamp, received_sig = (sig.split(",") + ["", ""])[:2]
expected = hmac.new(
RECURLY_WEBHOOK_KEY.encode(),
f"{timestamp}.{request.data.decode()}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, received_sig.split("=")[-1]):
return "", 401
payload = request.json
event_type = payload.get("event_type")
account_code = payload.get("account", {}).get("code", "")
deal_id = find_deal_by_field("recurly_account_code", account_code)
if not deal_id:
return "", 200
if event_type == "invoice.paid":
amount = payload.get("invoice", {}).get("total", 0)
create_kommo_note(deal_id,
f"Recurly: payment received - ${amount/100:.2f}")
elif event_type == "invoice.past_due":
create_kommo_note(deal_id, "Recurly: invoice overdue")
create_kommo_task(deal_id,
"Recurly: contact client - payment is overdue")
elif event_type == "subscription.canceled":
create_kommo_note(deal_id, "Recurly: subscription cancelled")
elif event_type == "subscription.reactivated":
create_kommo_note(deal_id, "Recurly: subscription reactivated")
return "", 200
Dunning Management: How Recurly Reduces Involuntary Churn
Recurly automatically retries failed payments on a configurable schedule (dunning cycles). For the Kommo integration, the key point is: when dunning ultimately fails and the subscription is cancelled, the subscription.canceled webhook appears as a Note -> the manager sees this in the deal card and can initiate a win-back call.
Retry schedule: Recurly Dashboard -> Configuration -> Dunning Campaigns. Standard cycle: days 1, 3, 7, 14, 21 after the first failure.
Real-World Case
B2B SaaS (US, 200+ subscribers, Kommo + Recurly):
- Before: Won in Kommo -> manager manually created an account in Recurly, added the plan, sent an invite. 20–30 minutes per new client. Sometimes the wrong plan was specified -> client on the incorrect tier.
- After: Won -> account + subscription in 10 seconds. Subscription UUID is saved in the deal -> on upgrade, the plan is changed via API with automatic proration. 0 plan errors over 8 months.
- Additionally:
invoice.past_due-> manager task on the day of the overdue. Involuntary churn decreased by 24% — the team responds before the dunning system cancels the subscription.
Who This Is Relevant For
- SaaS companies with 100+ active subscribers and complex plans (add-ons, usage-based)
- Companies with revenue recognition requirements (ASC 606) — Recurly has this built in
- Enterprise teams with procurement processes and multi-currency
- Companies where involuntary churn exceeds 3% — dunning management is critical
Frequently Asked Questions
Recurly vs Stripe Billing — when to choose which?
Stripe Billing: easier to start, good documentation, suitable for 0–500 subscriptions without complex pricing. Recurly: complex subscriptions, native dunning management, revenue recognition, 500+ clients with different plans. A separate guide for Kommo + Stripe is available — the architecture is similar, the difference lies in platform capabilities.
Does Recurly support EU VAT and tax compliance?
Yes. Recurly is integrated with Avalara for automatic tax calculation in 150+ jurisdictions including EU VAT. Setup: Recurly -> Integrations -> Avalara. On Won with an EU client — Recurly automatically applies the correct VAT rate.
How do I handle trial -> paid conversion via API?
Recurly supports trial subscriptions: when calling create_subscription with the trial_ends_at parameter. After the trial ends, Recurly automatically moves to paid. Webhook subscription.activated (trial -> paid) — Note in Kommo. Or subscription.canceled if the client did not convert.
Can I attach a payment method to an account via API?
Yes, via POST /accounts/{code}/billing_info with card data (or tokenised via Recurly.js). For B2B it is typically simpler: create account without card -> send client an invoice -> client pays via Recurly-hosted page. Hosted invoice payment does not require PCI compliance from your code.
Summary
- Recurly API: Basic Auth (api_key + empty password),
Accept: application/vnd.recurly.v2021-02-25 - Flow: create account -> create subscription -> save UUID in Kommo
- Plan change:
PUT /subscriptions/{uuid}withtimeframe: "now"— proration is automatic - Webhook: HMAC-SHA256 via
Recurly-Signature, key events:invoice.paid/past_due,subscription.canceled - Dunning: configure in Dashboard, on failure -> Note in Kommo via webhook
If you use Recurly and Kommo and want to automate subscription creation on Won — describe your plan structure and upgrade scenarios. Exceltic.dev will configure the integration with proration and dunning notifications.