Kommo + Flutterwave: платежи из воронки для рынков Африки и emerging markets

Flutterwave - ведущий платёжный gateway Африки: работает в 34+ странах, поддерживает mobile money (M-Pesa, MTN MoMo, Airtel Money), банковские переводы, карты и кошельки. Для B2B компаний, выходящих на африканские рынки с Kommo в качестве CRM, интеграция решает стандартную задачу: создать Payment Link при достижении определённого этапа воронки, получить webhook при успешной оплате и автоматически закрыть сделку.

Flutterwave API использует Bearer token. Payment Links API: POST /v3/payment-links - создать ссылку на оплату. Webhook: charge.completed - уведомление об успешном платеже. Ключевой параметр - tx_ref (transaction reference): произвольная строка, которую Flutterwave возвращает в webhook - используем для хранения kommo_lead_id.

Flutterwave Payment Link - размещённая страница оплаты с поддержкой всех местных методов: M-Pesa для Кении, MTN MoMo для Ганы/Уганды, Airtel Money для Замбии, карты Visa/Mastercard для всей Африки. Клиент выбирает удобный способ, вы получаете webhook при оплате.

Методы оплаты по странам

СтранаОсновные методы
НигерияБанковский перевод, карты, USSD
КенияM-Pesa, карты, банковский перевод
ГанаMTN MoMo, Vodafone Cash, карты
УгандаMTN MoMo, Airtel Money
Южная АфрикаКарты, EFT (банковский перевод)
ЕгипетКарты, Fawry, ValU

Все методы доступны через один Payment Link - Flutterwave сам определяет доступные способы по IP клиента.

Архитектура

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

Ваш сервер
  -> Kommo API: получить email, имя, сумму
  -> Flutterwave: POST /v3/payment-links
     {amount, currency, tx_ref: "kommo_{lead_id}", customer}
  -> Kommo: добавить note со ссылкой

Клиент оплачивает через M-Pesa / карту / Mobile Money
  -> Flutterwave webhook: charge.completed
  -> Ваш сервер: верифицировать подпись -> Kommo Closed Won
import requests, os, hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

FW_SECRET_KEY    = os.environ["FLUTTERWAVE_SECRET_KEY"]
FW_BASE          = "https://api.flutterwave.com/v3"
FW_HDR           = {"Authorization": f"Bearer {FW_SECRET_KEY}", "Content-Type": "application/json"}
FW_WEBHOOK_HASH  = os.environ["FLUTTERWAVE_WEBHOOK_HASH"]

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_ID    = int(os.environ["KOMMO_CLOSED_WON_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_contact(lead_id: int) -> tuple[dict, dict]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    lead     = r.json()
    contacts = lead.get("_embedded", {}).get("contacts", [])
    contact  = {}
    if contacts:
        rc = requests.get(
            f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
            headers=KOMMO_HDR,
            params={"with": "custom_fields_values"},
        )
        contact = rc.json()
    return lead, contact

def get_email(contact: dict) -> str:
    for cf in contact.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            if vals:
                return vals[0].get("value", "")
    return ""

def get_phone(contact: dict) -> str:
    for cf in contact.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "PHONE":
            vals = cf.get("values", [])
            if vals:
                return vals[0].get("value", "")
    return ""

def create_payment_link(amount: float, currency: str, lead_id: int,
                        name: str, email: str, phone: str) -> str:
    r = requests.post(
        f"{FW_BASE}/payment-links",
        headers=FW_HDR,
        json={
            "name":        f"Invoice #{lead_id}",
            "amount":      amount,
            "currency":    currency,  # NGN, KES, GHS, USD и др.
            "redirect_url": os.environ.get("PAYMENT_SUCCESS_URL", ""),
            "meta": [
                {"metaname": "kommo_lead_id", "metavalue": str(lead_id)},
            ],
            "is_permanent": False,
            "customer": {
                "name":        name,
                "email":       email,
                "phonenumber": phone,
            },
        },
    )
    r.raise_for_status()
    return r.json().get("data", {}).get("link", "")

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, contact = get_lead_contact(lead_id)
        amount   = float(lead.get("price") or 0)
        currency = os.environ.get("DEAL_CURRENCY", "USD")
        email    = get_email(contact)
        phone    = get_phone(contact)
        name     = contact.get("name", "")

        if amount <= 0 or not email:
            add_note(lead_id, "Flutterwave: сумма или email не указаны, создайте ссылку вручную.")
            continue

        link = create_payment_link(amount, currency, lead_id, name, email, phone)
        add_note(lead_id, f"Flutterwave Payment Link: {link}")

    return jsonify({"status": "ok"}), 200

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

def verify_flutterwave_webhook(secret_hash: str, received_hash: str) -> bool:
    # Flutterwave использует статичный Secret Hash (не HMAC)
    # Задаётся в Dashboard -> Settings -> Webhooks -> Secret Hash
    return hmac.compare_digest(secret_hash, received_hash)

@app.route("/webhooks/flutterwave", methods=["POST"])
def flutterwave_webhook():
    received = request.headers.get("verif-hash", "")
    if not verify_flutterwave_webhook(FW_WEBHOOK_HASH, received):
        return jsonify({"error": "invalid hash"}), 401

    event = request.json or {}
    if event.get("event") != "charge.completed":
        return jsonify({"status": "ignored"}), 200

    data   = event.get("data", {})
    status = data.get("status", "")
    if status != "successful":
        return jsonify({"status": "not_successful"}), 200

    # Извлечь kommo_lead_id из meta
    meta    = data.get("meta", {}) or {}
    lead_id = meta.get("kommo_lead_id", "")

    if not lead_id:
        # Попробовать tx_ref (fallback)
        tx_ref  = data.get("tx_ref", "")
        if tx_ref.startswith("kommo_"):
            lead_id = tx_ref.replace("kommo_", "")

    if not lead_id:
        return jsonify({"status": "no_lead_id"}), 200

    amount      = data.get("amount", 0)
    currency    = data.get("currency", "")
    flw_ref     = data.get("flw_ref", "")
    payment_type = data.get("payment_type", "")

    requests.patch(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        json={"status_id": CLOSED_WON_ID},
    )
    add_note(
        int(lead_id),
        f"Flutterwave: оплачено {amount} {currency} через {payment_type}. Ref: {flw_ref}",
    )
    return jsonify({"status": "ok"}), 200

Настройка Flutterwave webhook

Dashboard -> Settings -> Webhooks. URL: https://your-server.com/webhooks/flutterwave. Secret Hash: произвольная строка (используется в заголовке verif-hash, не HMAC). Активируйте событие charge.completed.

Flutterwave НЕ использует HMAC-SHA256 для webhook - вместо этого отправляет статичный Secret Hash в заголовке. Это менее безопасно чем HMAC, но достаточно если hash сложный и хранится в секрете.

Валюты и конвертация

Flutterwave поддерживает 35+ валют. Для B2B сделок в USD с оплатой в местных валютах используйте currency: "USD" в Payment Link - Flutterwave автоматически конвертирует по рыночному курсу. Сумма в webhook будет в исходной валюте платежа.

Верификация транзакции через API

После получения webhook рекомендуется верифицировать транзакцию напрямую:

def verify_transaction(transaction_id: int) -> bool:
    r = requests.get(
        f"{FW_BASE}/transactions/{transaction_id}/verify",
        headers=FW_HDR,
    )
    data = r.json().get("data", {})
    return data.get("status") == "successful"

flw_ref из webhook содержит id транзакции - передайте его в verify endpoint для подтверждения.

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

B2B SaaS и сервисные компании с клиентами в Нигерии, Кении, Гане, Уганде, ЮАР. Особенно актуально для EdTech, SaaS и профессиональных услуг, где клиент платит разово или по подписке. Flutterwave также работает в Европе и США через Stripe-подобный API для компаний с европейской регистрацией.

Аналогичные интеграции для других рынков: Kommo + Razorpay (India), Kommo + Mollie (Европа).

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

Как Flutterwave обрабатывает VAT и налоги для B2B в Нигерии?

Flutterwave не генерирует автоматически налоговые инвойсы. Для Нигерии VAT 7.5% нужно включать в сумму Payment Link (amount = стоимость услуги + VAT). Официальный FIRS-compliant инвойс оформляйте отдельно через бухгалтерский сервис (например, Wave или Zoho Books с нигерийским VAT модулем).

Как получить выплату: в какой валюте и на какой счёт?

Flutterwave поддерживает выплаты на банковские счета в Нигерии, Кении, ЮАР, Великобритании, США. Для неаффилированных стран - через Flutterwave Send Money или партнёрские банки. Конвертация происходит по курсу Flutterwave при создании выплаты. Минимальная сумма выплаты: $100 эквивалент.

Работает ли Flutterwave для recurring payments / подписок?

Да, через Flutterwave Subscriptions API: POST /v3/payment-plans - создать тарифный план, затем привязать клиента. Webhook subscription.charge при каждом списании. Для простых одноразовых платежей Payment Links достаточно - подписки нужны только для recurring billing.

Итог

Kommo + Flutterwave - платёжный gateway для Африки и EM:

  • Bearer token, POST /v3/payment-links с meta.kommo_lead_id
  • Верификация: статичный Secret Hash в заголовке verif-hash (не HMAC)
  • charge.completed webhook -> verify transaction -> Kommo Closed Won
  • 35+ валют, mobile money (M-Pesa, MTN MoMo) из коробки
  • Verify transaction через /v3/transactions/{id}/verify перед закрытием сделки

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

Ещё статьи

Все →