Kommo + Flutterwave: Payments from the Pipeline for Africa and Emerging Markets

Flutterwave is Africa’s leading payment gateway: it operates in 34+ countries and supports mobile money (M-Pesa, MTN MoMo, Airtel Money), bank transfers, cards, and wallets. For B2B companies entering African markets with Kommo as their CRM, the integration solves a standard problem: create a Payment Link when a deal reaches a specific pipeline stage, receive a webhook on successful payment, and automatically close the deal.

Flutterwave API uses a Bearer token. Payment Links API: POST /v3/payment-links - create a payment link. Webhook: charge.completed - notification of a successful payment. The key parameter is tx_ref (transaction reference): an arbitrary string that Flutterwave returns in the webhook - used to store kommo_lead_id.

Flutterwave Payment Link is a hosted payment page supporting all local methods: M-Pesa for Kenya, MTN MoMo for Ghana/Uganda, Airtel Money for Zambia, Visa/Mastercard cards across Africa. The customer chooses their preferred method, and you receive a webhook on payment.

Payment Methods by Country

CountryPrimary Methods
NigeriaBank transfer, cards, USSD
KenyaM-Pesa, cards, bank transfer
GhanaMTN MoMo, Vodafone Cash, cards
UgandaMTN MoMo, Airtel Money
South AfricaCards, EFT (bank transfer)
EgyptCards, Fawry, ValU

All methods are available through a single Payment Link - Flutterwave automatically determines available options based on the customer’s IP.

Architecture

Kommo: deal -> stage "Send Invoice"
  -> Kommo webhook leads.status.changed
  -> Your server

Your server
  -> Kommo API: get email, name, amount
  -> Flutterwave: POST /v3/payment-links
     {amount, currency, tx_ref: "kommo_{lead_id}", customer}
  -> Kommo: add note with the link

Customer pays via M-Pesa / card / Mobile Money
  -> Flutterwave webhook: charge.completed
  -> Your server: verify signature -> 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, etc.
            "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: amount or email not set, create the link manually.")
            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

Implementation: Webhook on Payment

def verify_flutterwave_webhook(secret_hash: str, received_hash: str) -> bool:
    # Flutterwave uses a static Secret Hash (not HMAC)
    # Set in 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

    # Extract kommo_lead_id from meta
    meta    = data.get("meta", {}) or {}
    lead_id = meta.get("kommo_lead_id", "")

    if not lead_id:
        # Try 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: paid {amount} {currency} via {payment_type}. Ref: {flw_ref}",
    )
    return jsonify({"status": "ok"}), 200

Configuring the Flutterwave Webhook

Dashboard -> Settings -> Webhooks. URL: https://your-server.com/webhooks/flutterwave. Secret Hash: an arbitrary string (sent in the verif-hash header, not HMAC). Enable the charge.completed event.

Flutterwave does NOT use HMAC-SHA256 for webhooks - instead it sends a static Secret Hash in the header. This is less secure than HMAC, but sufficient if the hash is complex and kept secret.

Currencies and Conversion

Flutterwave supports 35+ currencies. For B2B deals priced in USD but paid in local currencies, use currency: "USD" in the Payment Link - Flutterwave automatically converts at the market rate. The amount in the webhook will be in the original payment currency.

Transaction Verification via API

After receiving a webhook, it is recommended to verify the transaction directly:

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 from the webhook contains the transaction id - pass it to the verify endpoint for confirmation.

Who This Is For

B2B SaaS and service companies with clients in Nigeria, Kenya, Ghana, Uganda, and South Africa. Particularly relevant for EdTech, SaaS, and professional services where customers pay one-time or by subscription. Flutterwave also operates in Europe and the US via a Stripe-like API for companies with European registration.

Similar integrations for other markets: Kommo + Razorpay (India), Kommo + Mollie (Europe).

Frequently Asked Questions

How does Flutterwave handle VAT and taxes for B2B in Nigeria?

Flutterwave does not automatically generate tax invoices. For Nigeria, the 7.5% VAT must be included in the Payment Link amount (amount = service price + VAT). Issue an official FIRS-compliant invoice separately through an accounting service (e.g., Wave or Zoho Books with the Nigerian VAT module).

How are payouts handled: in which currency and to which account?

Flutterwave supports payouts to bank accounts in Nigeria, Kenya, South Africa, the UK, and the US. For countries not on this list - via Flutterwave Send Money or partner banks. Conversion happens at the Flutterwave rate at the time of payout. Minimum payout amount: $100 equivalent.

Does Flutterwave support recurring payments / subscriptions?

Yes, via the Flutterwave Subscriptions API: POST /v3/payment-plans - create a pricing plan, then attach a customer. The subscription.charge webhook fires on each billing cycle. For simple one-time payments, Payment Links are sufficient - subscriptions are only needed for recurring billing.

Summary

Kommo + Flutterwave - a payment gateway for Africa and emerging markets:

  • Bearer token, POST /v3/payment-links with meta.kommo_lead_id
  • Verification: static Secret Hash in the verif-hash header (not HMAC)
  • charge.completed webhook -> verify transaction -> Kommo Closed Won
  • 35+ currencies, mobile money (M-Pesa, MTN MoMo) out of the box
  • Verify transaction via /v3/transactions/{id}/verify before closing the deal

If your team works with African markets through Kommo and Flutterwave - reach out to the Exceltic.dev team.

More articles

All →