Razorpay is India’s leading payment gateway, covering 100+ payment methods: UPI, NetBanking, cards, wallets (Paytm, PhonePe), and EMI. It operates in India, Malaysia, and Singapore. For B2B SaaS companies targeting the Indian market, integrating Razorpay with Kommo solves a standard problem: create a Payment Link when a deal reaches a specific stage, and automatically move the deal to Closed Won once payment is received.
Razorpay API uses Basic Auth (Key ID + Key Secret). Payment Links API: POST /v1/payment_links - create a payment link. Webhooks: payment_link.paid and payment.captured - payment notifications. All amounts in Razorpay are in paise (1 INR = 100 paise).
Razorpay Payment Link - a payment page supporting all Indian payment methods. You can pass notes with arbitrary metadata - we use it to store kommo_lead_id.
Architecture
Kommo: deal -> stage "Send Invoice"
-> Kommo webhook: leads.status.changed
-> Your server
Your server
-> Razorpay API: POST /v1/payment_links
{amount_paise, description, notes.kommo_lead_id}
-> Kommo: record link as a note
Client pays via UPI/Card/NetBanking
-> Razorpay webhook: payment_link.paid
-> Your server: verify signature
-> Kommo: Closed Won + note with payment_id
Implementation: Creating a Payment Link
import requests, os, hmac, hashlib, json as json_mod
from flask import Flask, request, jsonify
app = Flask(__name__)
RZP_KEY_ID = os.environ["RAZORPAY_KEY_ID"]
RZP_KEY_SECRET = os.environ["RAZORPAY_KEY_SECRET"]
RZP_BASE = "https://api.razorpay.com/v1"
RZP_AUTH = (RZP_KEY_ID, RZP_KEY_SECRET)
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
INVOICE_STAGE_ID = int(os.environ["KOMMO_INVOICE_STAGE_ID"])
CLOSED_WON_STAGE = int(os.environ["KOMMO_CLOSED_WON_STAGE_ID"])
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}
def get_lead(lead_id: int) -> dict:
r = requests.get(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
params={"with": "contacts"},
)
return r.json()
def get_contact_details(contact_id: int) -> tuple[str, str, str]:
r = requests.get(
f"{KOMMO_BASE}/contacts/{contact_id}",
headers=KOMMO_HDR,
params={"with": "custom_fields_values"},
)
c = r.json()
email = ""
phone = ""
for cf in c.get("custom_fields_values", []) or []:
code = cf.get("field_code", "")
vals = cf.get("values", [])
if code == "EMAIL" and vals:
email = vals[0].get("value", "")
elif code == "PHONE" and vals:
phone = vals[0].get("value", "")
return c.get("name", ""), email, phone
def inr_to_paise(inr: float) -> int:
return int(inr * 100)
def create_payment_link(amount_inr: float, desc: str, lead_id: int,
contact_name: str, contact_email: str, contact_phone: str) -> str:
payload = {
"amount": inr_to_paise(amount_inr),
"currency": "INR",
"description": desc[:255],
"customer": {
"name": contact_name,
"email": contact_email,
"contact": contact_phone,
},
"notes": {
"kommo_lead_id": str(lead_id),
},
"reminder_enable": True,
"notify": {
"sms": bool(contact_phone),
"email": bool(contact_email),
},
}
r = requests.post(f"{RZP_BASE}/payment_links", auth=RZP_AUTH, json=payload)
r.raise_for_status()
return r.json().get("short_url", "")
def add_note(lead_id: int, text: str):
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": lead_id,
"entity_type": "leads",
"note_type": "common",
"params": {"text": text},
}],
)
@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
data = request.json or {}
for lead_data in data.get("leads", {}).get("status", []):
lead_id = lead_data.get("id")
new_status = lead_data.get("status_id")
if new_status != INVOICE_STAGE_ID:
continue
lead = get_lead(lead_id)
budget = lead.get("price", 0) or 0
name = lead.get("name", f"Deal #{lead_id}")
contacts = lead.get("_embedded", {}).get("contacts", [])
cname = cemail = cphone = ""
if contacts:
cname, cemail, cphone = get_contact_details(contacts[0]["id"])
if budget <= 0:
add_note(lead_id, "Razorpay: deal amount not specified, please create a Payment Link manually.")
continue
link = create_payment_link(float(budget), name, lead_id, cname, cemail, cphone)
add_note(lead_id, f"Razorpay Payment Link: {link}")
return jsonify({"status": "ok"}), 200
Implementation: Payment Webhook
def verify_razorpay_webhook(body: bytes, signature: str) -> bool:
# Razorpay HMAC-SHA256 with Key Secret
digest = hmac.new(RZP_KEY_SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, signature)
@app.route("/webhooks/razorpay", methods=["POST"])
def razorpay_webhook():
sig = request.headers.get("X-Razorpay-Signature", "")
if not verify_razorpay_webhook(request.data, sig):
return jsonify({"error": "invalid signature"}), 401
event = request.json or {}
ev = event.get("event", "")
if ev not in ("payment_link.paid", "payment.captured"):
return jsonify({"status": "ignored"}), 200
if ev == "payment_link.paid":
pl = event.get("payload", {}).get("payment_link", {}).get("entity", {})
notes = pl.get("notes", {})
lead_id = notes.get("kommo_lead_id", "")
amount = pl.get("amount", 0) / 100 # paise -> INR
pay_id = event.get("payload", {}).get("payment", {}).get("entity", {}).get("id", "")
else:
pay_entity = event.get("payload", {}).get("payment", {}).get("entity", {})
notes = pay_entity.get("notes", {})
lead_id = notes.get("kommo_lead_id", "")
amount = pay_entity.get("amount", 0) / 100
pay_id = pay_entity.get("id", "")
if not lead_id:
return jsonify({"status": "no_lead_id"}), 200
requests.patch(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
json={"status_id": CLOSED_WON_STAGE},
)
add_note(
int(lead_id),
f"Razorpay: payment received INR {amount:.2f}. Payment ID: {pay_id}",
)
return jsonify({"status": "ok"}), 200
Configuring the Razorpay Webhook
- Razorpay Dashboard -> Settings -> Webhooks -> Add New Webhook
- URL:
https://your-server.com/webhooks/razorpay - Events:
payment_link.paid,payment.captured - Secret: any string -> use it as the signing secret
Razorpay signs every webhook with X-Razorpay-Signature - a hex HMAC-SHA256 of the request body using the webhook secret (not the Key Secret). Make sure you are using the correct secret in verify_razorpay_webhook.
UPI and the Diversity of Payment Methods
Razorpay Payment Link automatically displays all available payment methods:
- UPI (GPay, PhonePe, Paytm, BHIM) - 70%+ of transactions in India
- NetBanking - all major banks
- Cards (Visa, Mastercard, RuPay)
- Wallets (Paytm, Amazon Pay)
- EMI (card-free, through banks)
No additional configuration needed - everything is available by default.
For International Payments
Razorpay supports 100+ currencies through Razorpay International (requires separate activation). A standard account is sufficient for INR payments. For Malaysia/Singapore - Razorpay Curlec (a separate product).
Real-World Case
A B2B SaaS company focused on India, 40 deals per month, average deal size 25,000 INR. Before the integration: managers created Payment Links manually in the Razorpay Dashboard. After: the link is generated automatically when a deal moves to the “Payment” stage. Razorpay automatically sends a WhatsApp reminder to the client via reminder_enable.
Who This Is For
B2B SaaS and service companies with clients in India. Especially relevant when UPI is the primary payment method for clients. Developers from India often build products on Razorpay as their first payment gateway before expanding to global markets.
A similar integration for the European market is described for Kommo + Mollie and Kommo + GoCardless.
Frequently Asked Questions
How does Razorpay handle GST for B2B deals in India?
Razorpay supports GST in invoices. When creating a Payment Link, you can add line_items with tax_amount. For B2B deals with GST, a separate tax invoice is required - Razorpay generates it automatically when GST registration is properly configured in the Dashboard.
Are there limits on amounts in Razorpay Payment Links?
The maximum amount for a single Payment Link is 500,000 INR (~$6,000). For larger deals, create multiple links or use the Razorpay Invoice API with installment splits. For enterprise deals (>10 lakhs INR), Razorpay NACH (direct debit) is the better option.
How does a refund work through the Razorpay API?
POST /v1/payments/{payment_id}/refund with {amount: paise}. Supports partial or full refunds. After issuing a refund, update the deal status in Kommo via the Kommo API - Razorpay does not notify about a successful refund through the same webhook; you need to listen for the separate refund.processed event.
Summary
Kommo + Razorpay - payment gateway for India:
- Basic Auth (Key ID + Key Secret), amounts in paise (INR x 100)
notes.kommo_lead_idfor correlating webhook -> deal- Webhook
payment_link.paid-> HMAC-SHA256 verification -> Closed Won reminder_enable: true- Razorpay automatically reminds the client- UPI + NetBanking + cards + wallets out of the box, no extra configuration
If your team works with the Indian market via Razorpay and Kommo - describe your challenge to the Exceltic.dev team.