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:
- Deal moves to the “Won” stage in Kommo
- Kommo webhook sends contact data to our service
- Service creates a Customer in the Braintree vault and generates a payment link
- The link is added as a Note to the deal card
Scenario 2 - payment confirmed:
- Braintree sends a
transaction.settledwebhook - 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 onbt_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.