Kommo + Square: Accepting Payments from Your Sales Pipeline Without Manual Invoicing
Square is a payment platform built for small and medium businesses - covering terminals, online payments, invoices, and subscriptions. It is widely used in the US, Canada, Australia, the UK, and Japan. For B2B SaaS and service companies, Square offers Payment Links and an Invoices API, letting you collect payment without a physical terminal. A custom integration with Kommo lets you generate a payment link directly from a deal card and automatically move the deal to Closed Won once payment is received.
The Square Payments API is built on OAuth 2.0 and nonce tokens (one-time tokens from the Web Payments SDK). For server-side operations there is the Payment Links API: create a link, send it to the client, and receive a payment notification via webhook.
Payment Link (Square) - a single-use or reusable payment page with a fixed amount and description. Created via API, requires no website or POS terminal.
The Problem: Manual Invoicing
The typical process without an integration: deal closes in Kommo -> manager opens the Square Dashboard -> manually creates an invoice or Payment Link -> copies the link into Kommo -> waits for payment -> manually marks the deal as paid.
This process takes 10 - 15 minutes per deal. With 20+ deals per month that adds up to 3 - 5 hours of routine work. Paid deals stay stuck in “awaiting payment” status until someone updates them by hand.
Integration Architecture
Kommo: deal moves to "Send Invoice" stage
-> Kommo webhook: leads.status.changed
-> Your server
Your server
-> Square API: create Payment Link
{amount: deal.budget, note: deal.name, kommo_lead_id: deal.id}
-> Kommo API: add link to deal card as a note
Client pays -> Square
-> Square webhook: payment.completed
{order.reference_id: kommo_lead_id}
-> Your server
-> Kommo API: move deal to Closed Won
-> Kommo API: update custom field "Paid"
Implementation: Creating a Payment Link on Stage Change
import requests, uuid, os
from flask import Flask, request, jsonify
app = Flask(__name__)
SQUARE_ACCESS_TOKEN = os.environ["SQUARE_ACCESS_TOKEN"]
SQUARE_LOCATION_ID = os.environ["SQUARE_LOCATION_ID"]
SQUARE_WEBHOOK_SIG = os.environ["SQUARE_WEBHOOK_SIGNATURE_KEY"]
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"] # mycompany
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
INVOICE_STAGE_ID = int(os.environ["KOMMO_INVOICE_STAGE_ID"])
CLOSED_WON_STAGE_ID = int(os.environ["KOMMO_CLOSED_WON_STAGE_ID"])
SQUARE_BASE = "https://connect.squareup.com/v2"
SQUARE_HDR = {
"Authorization": f"Bearer {SQUARE_ACCESS_TOKEN}",
"Content-Type": "application/json",
"Square-Version": "2024-05-15",
}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {
"Authorization": f"Bearer {KOMMO_TOKEN}",
"Content-Type": "application/json",
}
def get_lead(lead_id: int) -> dict:
r = requests.get(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
params={"with": "contacts,custom_fields_values"},
)
return r.json()
def create_payment_link(amount_usd: float, note: str, reference_id: str) -> str:
# Square works in cents
amount_cents = int(amount_usd * 100)
payload = {
"idempotency_key": str(uuid.uuid4()),
"quick_pay": {
"name": note,
"price_money": {"amount": amount_cents, "currency": "USD"},
"location_id": SQUARE_LOCATION_ID,
},
"checkout_options": {
"redirect_url": "",
"ask_for_shipping_address": False,
},
"pre_populated_data": {},
"payment_note": reference_id, # kommo_lead_id for back-reference
"order": {
"reference_id": reference_id, # returned in webhook
},
}
r = requests.post(f"{SQUARE_BASE}/online-checkout/payment-links", headers=SQUARE_HDR, json=payload)
r.raise_for_status()
return r.json()["payment_link"]["url"]
def add_note_to_lead(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 = get_lead(lead_id)
budget = lead.get("price", 0) or 0
name = lead.get("name", f"Deal #{lead_id}")
if budget <= 0:
add_note_to_lead(lead_id, "Square: deal amount not set. Please create the Payment Link manually.")
continue
link = create_payment_link(
amount_usd=float(budget),
note=name,
reference_id=str(lead_id),
)
add_note_to_lead(lead_id, f"Square Payment Link created: {link}")
return jsonify({"status": "ok"}), 200
Implementation: Webhook on Successful Payment
import hmac, hashlib, base64
def verify_square_signature(body: bytes, signature: str, sig_key: str, url: str) -> bool:
# Square HMAC-SHA256: base64(hmac(url + body, key))
msg = (url + body.decode("utf-8")).encode("utf-8")
secret = sig_key.encode("utf-8")
digest = hmac.new(secret, msg, hashlib.sha256).digest()
computed = base64.b64encode(digest).decode()
return hmac.compare_digest(computed, signature)
@app.route("/webhooks/square", methods=["POST"])
def square_webhook():
sig = request.headers.get("x-square-hmacsha256-signature", "")
full_url = request.url # must match the URL registered in Square Developer Dashboard
if not verify_square_signature(request.data, sig, SQUARE_WEBHOOK_SIG, full_url):
return jsonify({"error": "invalid signature"}), 401
event = request.json or {}
if event.get("type") != "payment.completed":
return jsonify({"status": "ignored"}), 200
payment = event.get("data", {}).get("object", {}).get("payment", {})
order_ref = payment.get("order_id", "")
# reference_id is stored on the order - requires a separate request
order_id = payment.get("order_id", "")
if order_id:
r_order = requests.get(f"{SQUARE_BASE}/orders/{order_id}", headers=SQUARE_HDR)
order = r_order.json().get("order", {})
kommo_id = order.get("reference_id", "")
else:
kommo_id = ""
if not kommo_id:
return jsonify({"status": "no_lead_id"}), 200
amount_cents = payment.get("amount_money", {}).get("amount", 0)
amount_usd = amount_cents / 100.0
# Move deal to Closed Won
requests.patch(
f"{KOMMO_BASE}/leads/{kommo_id}",
headers=KOMMO_HDR,
json={"status_id": CLOSED_WON_STAGE_ID},
)
# Add payment note
add_note_to_lead(
int(kommo_id),
f"Square: payment received ${amount_usd:.2f}. Payment ID: {payment.get('id', '')}",
)
return jsonify({"status": "ok"}), 200
Setting Up the Square Developer Portal
- Go to https://developer.squareup.com/ -> Applications -> create an App
- Obtain an Access Token (Production or Sandbox for testing)
- Get your Location ID: Square Dashboard -> Locations
- Webhooks: Developer Portal -> Webhooks -> Add endpoint
- URL:
https://your-server.com/webhooks/square - Events:
payment.completed,payment.failed,order.updated
- URL:
- Copy the Signature Key from the webhook settings
Real-World Case
A consulting agency with 6 sales reps, an average deal size of $2,400, and 25 deals per month. Square was the primary payment collection tool; Kommo managed the sales pipeline. Without the integration, reps were spending 2 - 3 hours per week creating Payment Links and updating deal statuses.
After the integration: a Payment Link is generated automatically when a deal moves to the “Send Invoice” stage. Once paid, the deal moves to Closed Won with no manual input required. Time saved: roughly 2.5 hours per week across the team.
Who This Is For
Service companies and B2B SaaS businesses in the US, Canada, and Australia that use Square as their primary payment tool - especially those with average deal sizes of $500 - $5,000 and sales teams of 3 - 15 people.
If your company uses a different payment gateway, the same approach is covered for Kommo + Stripe and Kommo + Paddle.
Frequently Asked Questions
Does Square support recurring payments (subscriptions)?
Yes. Square Subscriptions API: POST /v2/subscriptions with plan_variation_id and card_id. The customer’s card must be saved in advance via the Web Payments SDK. For B2B SaaS with monthly billing this is a full-featured alternative to Stripe Billing.
Which countries does Square operate in?
The US, Canada, Australia, the UK, Japan, Ireland, France, and Spain. Square is less common across the EU as a whole - Stripe, Mollie, or Checkout.com tend to be better fits there.
How do I test without real payments?
Square provides a Sandbox environment with separate API keys. The Sandbox Access Token and Location ID are obtained in the Developer Dashboard independently of Production. Test cards: 4111 1111 1111 1111 (Visa), any 3-digit CVV.
Summary
Kommo + Square - automating payment collection from your pipeline:
- Kommo webhook
leads.status.changed-> create Square Payment Link order.reference_id=kommo_lead_idfor correlation- Square webhook
payment.completed-> Closed Won + note - HMAC-SHA256 verification:
base64(hmac(url + body, sig_key)) - Idempotency: UUID
idempotency_keyon payment creation
If your team uses Square and Kommo - describe your requirements to the Exceltic.dev team. We will build the integration to fit your exact stack.