Kommo + Braintree: payment gateway integration from the sales pipeline

Braintree (PayPal) is one of the few gateways with a full REST API for subscriptions, one-time transactions, and vault-based card storage. There is no native integration with Kommo. The standard Zapier approach doesn’t cover webhook verification and doesn’t update deals atomically. The right solution is a custom integration via Braintree REST API v1.

Why the native integration doesn’t work

Braintree doesn’t publish ready-made CRM connectors. Zapier can create transactions but doesn’t verify incoming webhooks. This creates two problems:

  • A payment in Braintree is not automatically reflected in the Kommo deal card
  • Transaction status (settled, failed, disputed) never reaches the pipeline

Companies log payments manually or via exports from the Braintree Control Panel - a delay of anywhere from a few hours to a full day.

What we’re building

Scenario 1 - deal won:

  1. Deal moves to the “Won” stage in Kommo
  2. Kommo webhook sends contact data to our service
  3. Service creates a Customer in the Braintree vault and generates a payment link
  4. The link is added as a Note to the deal card

Scenario 2 - payment confirmed:

  1. Braintree sends a transaction.settled webhook
  2. Service verifies the signature and updates the Kommo deal: payment amount, date, status

Braintree authentication

Braintree uses HTTP Basic Auth: Merchant ID + Private Key encoded as merchant_id:private_key in Base64.

import base64, requests

MERCHANT_ID = "your_merchant_id"
PRIVATE_KEY = "your_private_key"
PUBLIC_KEY  = "your_public_key"

credentials = base64.b64encode(f"{PUBLIC_KEY}:{PRIVATE_KEY}".encode()).decode()

session = requests.Session()
session.headers.update({
    "Authorization": f"Basic {credentials}",
    "Content-Type": "application/json",
    "Braintree-Version": "2023-08-01",
})

BT_BASE = "https://payments.sandbox.braintreegateway.com"  # prod: payments.braintreegateway.com

Merchant ID is used in the URL. Public Key + Private Key go into Basic Auth.

Creating a customer and transaction

Creating a Customer in the vault:

def create_customer(kommo_contact: dict) -> str:
    """Create Braintree customer from Kommo contact. Returns customerId."""
    payload = {
        "customer": {
            "firstName": kommo_contact.get("first_name", ""),
            "lastName":  kommo_contact.get("last_name", ""),
            "email":     kommo_contact.get("email", ""),
            "company":   kommo_contact.get("company", ""),
            "customFields": {
                "kommo_contact_id": str(kommo_contact["id"]),
            },
        }
    }
    r = session.post(f"{BT_BASE}/merchants/{MERCHANT_ID}/customers", json=payload)
    r.raise_for_status()
    return r.json()["customer"]["id"]

Creating a payment link (hosted fields):

def create_payment_link(customer_id: str, amount: float, deal_id: int) -> str:
    """Generate Braintree hosted payment link. Returns URL."""
    payload = {
        "paymentLink": {
            "amount": f"{amount:.2f}",
            "currency": "EUR",
            "customerId": customer_id,
            "orderId": f"kommo-deal-{deal_id}",
            "expiresAt": "2026-12-31T23:59:59Z",
        }
    }
    r = session.post(f"{BT_BASE}/merchants/{MERCHANT_ID}/payment_links", json=payload)
    r.raise_for_status()
    return r.json()["paymentLink"]["url"]

The link is added as a Note to Kommo via /api/v4/leads/{lead_id}/notes.

Braintree webhook verification

Braintree sends webhooks as an HTTP POST with two form parameters: bt_signature and bt_payload. This is not JSON - it’s a standard application/x-www-form-urlencoded form.

import hashlib, hmac

def verify_braintree_webhook(bt_signature: str, bt_payload: str) -> bool:
    """Verify Braintree webhook. Both params come as form fields, not JSON."""
    # Braintree uses HMAC-SHA1 (not SHA-256) for webhook verification
    expected = hmac.new(
        PRIVATE_KEY.encode(),
        bt_payload.encode(),
        hashlib.sha1,
    ).hexdigest()
    # bt_signature format: "public_key|hash_value"
    parts = bt_signature.split("|")
    if len(parts) != 2 or parts[0] != PUBLIC_KEY:
        return False
    return hmac.compare_digest(parts[1], expected)

The key difference from other gateways: Braintree uses HMAC-SHA1 (not SHA-256) and passes the signature together with the public key separated by |.

Processing a transaction webhook

import base64, xml.etree.ElementTree as ET
from flask import Flask, request, abort

app = Flask(__name__)

@app.route("/braintree/webhook", methods=["POST"])
def braintree_webhook():
    bt_signature = request.form.get("bt_signature", "")
    bt_payload   = request.form.get("bt_payload", "")

    if not verify_braintree_webhook(bt_signature, bt_payload):
        abort(401)

    # Payload is base64-encoded XML
    xml_data = base64.b64decode(bt_payload).decode("utf-8")
    root = ET.fromstring(xml_data)

    kind = root.findtext("kind")
    if kind != "transaction_settled":
        return "ok", 200

    transaction = root.find(".//transaction")
    amount      = transaction.findtext("amount")
    order_id    = transaction.findtext("order-id")   # "kommo-deal-{id}"
    status      = transaction.findtext("status")

    deal_id = int(order_id.replace("kommo-deal-", ""))
    update_kommo_deal(deal_id, amount, status)
    return "ok", 200

def update_kommo_deal(deal_id: int, amount: str, status: str):
    """Add payment note and update deal custom field in Kommo."""
    note_text = f"Braintree payment {status}: {amount} EUR"
    requests.post(
        f"https://YOUR.kommo.com/api/v4/leads/{deal_id}/notes",
        headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
        json={"add": [{"note_type": "common", "params": {"text": note_text}}]},
    )

Braintree payload arrives as base64-encoded XML - not JSON. This is unusual for modern gateways.

Refund scenario

def refund_transaction(transaction_id: str, amount: float = None) -> dict:
    """Full or partial refund. amount=None means full refund."""
    payload = {}
    if amount:
        payload = {"transaction": {"amount": f"{amount:.2f}"}}

    r = session.post(
        f"{BT_BASE}/merchants/{MERCHANT_ID}/transactions/{transaction_id}/refund",
        json=payload,
    )
    r.raise_for_status()
    return r.json()

The refund webhook (transaction.refunded) is handled the same way - a Note with the refund amount is added to Kommo.

Real case

A EU SaaS company with 120+ enterprise clients was logging payments into Kommo manually. Managers checked the Braintree Control Panel once a day and copied data into deals.

After the integration:

  • Payment status appears in Kommo within 30 seconds of transaction.settled
  • Payment link is generated automatically when the deal moves to “Won”
  • Refunds are logged as Notes immediately - no delay until the next business day

Manual processing time per payment dropped from ~7 minutes to zero.

Who this is for

Companies already using Braintree as their primary gateway who want to see the full payment history in Kommo. Especially relevant for SaaS with one-time payments or hybrid models (subscription + one-time).

If you use Stripe - see Kommo + Stripe: payment links from the pipeline. For GoCardless (SEPA direct debit) - there’s a separate guide.

Frequently asked questions

How does Braintree differ from Stripe in terms of API?

Both gateways provide a REST API. Key differences: Braintree uses Basic Auth (Public Key + Private Key), webhooks are verified via HMAC-SHA1 with form parameters, and payloads arrive as base64 XML. Stripe uses a Bearer token, JSON payload, and HMAC-SHA256.

For new projects Stripe is easier to integrate. Braintree is chosen when it’s already embedded in the PayPal ecosystem or when Vault storage for cards is needed.

How is customer card data stored?

Braintree Vault stores cards on Braintree’s side - you don’t need PCI DSS certification. You work with payment_method_nonce or payment_method_token. For repeat transactions you use the saved token without asking the customer to re-enter their card.

Does this work with Braintree subscriptions?

Yes. Braintree Plans and Subscriptions are connected similarly: when a deal is won, a Subscription is created; the subscription.charged_successfully webhook is logged in Kommo. Dunning (failed charge attempts) - subscription.went_past_due creates a task for the manager.

How to test without real payments?

Braintree provides a sandbox environment (payments.sandbox.braintreegateway.com) with test card numbers. Sandbox webhooks can be simulated via Braintree Control Panel > Webhooks > Test. For CI/CD - use a separate sandbox merchant account.

Summary

The Kommo + Braintree integration closes the gap between an actual payment and the deal status in the CRM. Key points:

  • Authentication via Basic Auth (Public Key + Private Key)
  • Webhook verification: bt_signature|public_key + HMAC-SHA1 on bt_payload
  • Payload is base64 XML, not JSON
  • Payment link is generated automatically on transition to Won

If you work with Braintree and want to set up the integration for your stack - describe the task to the Exceltic.dev team. We’ll review the architecture and estimate the scope of work.

More articles

All →