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
| Country | Primary Methods |
|---|---|
| Nigeria | Bank transfer, cards, USSD |
| Kenya | M-Pesa, cards, bank transfer |
| Ghana | MTN MoMo, Vodafone Cash, cards |
| Uganda | MTN MoMo, Airtel Money |
| South Africa | Cards, EFT (bank transfer) |
| Egypt | Cards, 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
Implementation: Creating a Payment Link
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-linkswithmeta.kommo_lead_id - Verification: static Secret Hash in the
verif-hashheader (not HMAC) charge.completedwebhook -> verify transaction -> Kommo Closed Won- 35+ currencies, mobile money (M-Pesa, MTN MoMo) out of the box
- Verify transaction via
/v3/transactions/{id}/verifybefore closing the deal
If your team works with African markets through Kommo and Flutterwave - reach out to the Exceltic.dev team.