Kommo + Razorpay: платёжный gateway для India и emerging markets из воронки продаж

Razorpay - ведущий платёжный gateway Индии с охватом 100+ методов оплаты: UPI, NetBanking, карты, кошельки (Paytm, PhonePe), EMI. Работает в Индии, Малайзии, Сингапуре. Для B2B SaaS компаний, работающих с индийским рынком, интеграция Razorpay с Kommo решает стандартную задачу: создать Payment Link при достижении сделкой определённого этапа и автоматически перевести сделку в Closed Won при получении оплаты.

Razorpay API использует Basic Auth (Key ID + Key Secret). Payment Links API: POST /v1/payment_links - создать ссылку на оплату. Webhook: payment_link.paid и payment.captured - уведомление об оплате. Все суммы в Razorpay в пайсах (1 INR = 100 paise).

Razorpay Payment Link - страница оплаты с поддержкой всех индийских методов оплаты. Можно передать notes с произвольными метаданными - используем для хранения kommo_lead_id.

Архитектура

Kommo: сделка -> этап "Выставить счёт"
  -> Kommo webhook: leads.status.changed
  -> Ваш сервер

Ваш сервер
  -> Razorpay API: POST /v1/payment_links
     {amount_paise, description, notes.kommo_lead_id}
  -> Kommo: записать ссылку как note

Клиент оплачивает через UPI/Card/NetBanking
  -> Razorpay webhook: payment_link.paid
  -> Ваш сервер: верифицировать подпись
  -> Kommo: Closed Won + note с payment_id
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: сумма сделки не указана, создайте Payment Link вручную.")
            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

Реализация: webhook при оплате

def verify_razorpay_webhook(body: bytes, signature: str) -> bool:
    # Razorpay HMAC-SHA256 с 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: оплачено INR {amount:.2f}. Payment ID: {pay_id}",
    )
    return jsonify({"status": "ok"}), 200

Настройка Razorpay webhook

  1. Razorpay Dashboard -> Settings -> Webhooks -> Add New Webhook
  2. URL: https://your-server.com/webhooks/razorpay
  3. Events: payment_link.paid, payment.captured
  4. Secret: любая строка -> использовать как подпись

Razorpay подписывает каждый webhook с X-Razorpay-Signature - hex HMAC-SHA256 тела запроса с webhook secret (не Key Secret). Убедитесь что используете правильный секрет в verify_razorpay_webhook.

UPI и многообразие методов оплаты

Razorpay Payment Link автоматически показывает все доступные методы оплаты:

  • UPI (GPay, PhonePe, Paytm, BHIM) - 70%+ транзакций в Индии
  • NetBanking - все крупные банки
  • Карты (Visa, Mastercard, RuPay)
  • Кошельки (Paytm, Amazon Pay)
  • EMI (без карты, через банки)

Никакой дополнительной настройки - всё доступно по умолчанию.

Для международных платежей

Razorpay поддерживает 100+ валют через Razorpay International (отдельное подключение). Для INR-платежей достаточно стандартного аккаунта. Для Malaysia/Singapore - Razorpay Curlec (отдельный продукт).

Реальный кейс

B2B SaaS, ориентированный на Индию, 40 сделок в месяц, средний чек 25 000 INR. До интеграции: менеджеры создавали Payment Links вручную в Razorpay Dashboard. После: ссылка создаётся при переводе сделки в этап “Оплата”. Razorpay автоматически отправляет WhatsApp-напоминание клиенту через reminder_enable.

Для кого актуально

B2B SaaS и сервисные компании с клиентами в Индии. Особенно если UPI - основной метод оплаты клиентов. Разработчики из Индии часто строят продукты на Razorpay как первом платёжном gateway перед выходом на глобальный рынок.

Аналогичная интеграция для европейского рынка описана для Kommo + Mollie и Kommo + GoCardless.

Часто задаваемые вопросы

Как Razorpay обрабатывает GST для B2B в Индии?

Razorpay поддерживает GST в инвойсах. При создании Payment Link можно добавить line_items с tax_amount. Для B2B сделок с GST нужен отдельный налоговый инвойс - Razorpay генерирует его автоматически при правильной настройке GST регистрации в Dashboard.

Максимальная сумма одного Payment Link: 500 000 INR (~$6 000). Для больших сделок создавайте несколько ссылок или используйте Razorpay Invoice API с разбивкой на части. Для enterprise-сделок (>10 lakhs INR) лучше использовать Razorpay NACH (прямое дебетование).

Как работает возврат через Razorpay API?

POST /v1/payments/{payment_id}/refund с {amount: paise}. Частичный или полный возврат. После возврата обновите статус сделки в Kommo через Kommo API - Razorpay не уведомляет об успешном возврате через тот же webhook, нужно слушать отдельное событие refund.processed.

Итог

Kommo + Razorpay - платёжный gateway для India:

  • Basic Auth (Key ID + Key Secret), суммы в пайсах (INR x 100)
  • notes.kommo_lead_id для корреляции webhook -> сделка
  • Webhook payment_link.paid -> HMAC-SHA256 верификация -> Closed Won
  • reminder_enable: true - Razorpay автоматически напоминает клиенту
  • UPI + NetBanking + карты + кошельки из коробки без дополнительной настройки

Если ваша команда работает с индийским рынком через Razorpay и Kommo - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →