Kommo + QuickBooks: Automatic Invoices from the Sales Pipeline

QuickBooks Online is the dominant accounting platform in the US, Canada, and parts of the EU market. The QuickBooks REST API (Intuit API v3) supports customer creation, invoice generation, payment status retrieval, and webhook notifications. The Kommo integration closes the standard pattern: deal Won -> Customer in QuickBooks -> Invoice -> after payment the CRM is updated, with no manual data entry.

QuickBooks vs Zoho Books vs Wave: When to Choose QuickBooks

ParameterQuickBooks OnlineZoho BooksWave
APIREST v3RESTGraphQL
Dominant marketUS, CanadaGlobalUS, Canada
Price (Simple Start)$35/monthFree up to $50kFree (basic)
Multi-currencyPlus and aboveAll plansPartial
Built-in payment processingYes (QuickBooks Payments)YesUS/Canada
Ecosystem750+ integrationsZoho 50+ productsIndependent

QuickBooks is chosen by companies working with US/CA clients or auditors — it is the de facto standard in North America. Comparisons with Zoho Books and Wave are covered in separate articles.

What Gets Synchronised

Kommo -> QuickBooks: — Deal contact -> Customer in QB (deduplication by email via query API) — Deal name and amount -> Invoice line items — Payment terms from custom field -> Terms (Net 15, Net 30, etc.) — Invoice ID and link -> custom fields in Kommo card

QuickBooks -> Kommo: — Webhook Payment with txnStatus = Completed -> “Paid” field in deal — Move deal to “Payment Received” stage — Payment date and amount -> Note on deal

Architecture

Kommo Webhook: deal Won
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> name, email, amount, payment terms
  2. QB API: POST /v3/company/{realmId}/query
     -> SELECT * FROM Customer WHERE PrimaryEmailAddr = '{email}'
     -> found: use Id
     -> not found: POST /v3/company/{realmId}/customer
  3. QB API: POST /v3/company/{realmId}/invoice
     -> CustomerRef + Line items + DueDate
     -> get Id, DocNumber, InvoiceLink
  4. QB API: POST /v3/company/{realmId}/invoice/{id}/send
     -> send invoice to client by email
  5. Kommo: PATCH /leads/{id}
     -> update fields qb_invoice_id, invoice_url

QuickBooks Webhook: Payment (txnStatus = Completed)
  ↓ Backend
  1. GET /v3/company/{realmId}/payment/{paymentId}
     -> find LinkedTxn with Invoice Id
  2. GET from storage: find kommo_deal_id by qb_invoice_id
  3. Kommo: PATCH /leads/{deal_id}
     -> stage -> "Paid", field payment_date

QuickBooks REST API: Key Requests

QuickBooks API uses OAuth 2.0 (Authorization Code Flow). All requests go to https://quickbooks.api.intuit.com/v3/company/{realmId}/. Required parameter: ?minorversion=75.

Find or create Customer:

import requests

QB_BASE = f'https://quickbooks.api.intuit.com/v3/company/{REALM_ID}'
HEADERS = {
    'Authorization': f'Bearer {access_token}',
    'Accept': 'application/json',
    'Content-Type': 'application/json'
}
PARAMS = {'minorversion': '75'}

def find_or_create_customer(email: str, display_name: str, company: str) -> str:
    # Search by email
    query = f"SELECT * FROM Customer WHERE PrimaryEmailAddr = '{email}'"
    resp = requests.post(
        f'{QB_BASE}/query',
        params={**PARAMS, 'query': query},
        headers=HEADERS
    )
    customers = resp.json()['QueryResponse'].get('Customer', [])
    if customers:
        return customers[0]['Id']

    # Create new customer
    payload = {
        'DisplayName': display_name,
        'CompanyName': company,
        'PrimaryEmailAddr': {'Address': email}
    }
    resp = requests.post(
        f'{QB_BASE}/customer',
        params=PARAMS,
        json=payload,
        headers=HEADERS
    )
    return resp.json()['Customer']['Id']

Create invoice:

from datetime import date, timedelta

def create_invoice(customer_id: str, deal_name: str, amount: float,
                   due_days: int = 30) -> dict:
    today = date.today().isoformat()
    due_date = (date.today() + timedelta(days=due_days)).isoformat()

    payload = {
        'CustomerRef': {'value': customer_id},
        'DueDate': due_date,
        'TxnDate': today,
        'Line': [
            {
                'Amount': amount,
                'DetailType': 'SalesItemLineDetail',
                'SalesItemLineDetail': {
                    'ItemRef': {'value': '1', 'name': 'Services'},  # item from QB
                    'Qty': 1,
                    'UnitPrice': amount
                },
                'Description': deal_name
            }
        ]
    }
    resp = requests.post(
        f'{QB_BASE}/invoice',
        params=PARAMS,
        json=payload,
        headers=HEADERS
    )
    invoice = resp.json()['Invoice']
    return {'id': invoice['Id'], 'doc_number': invoice['DocNumber']}

Send invoice to client:

def send_invoice(invoice_id: str, client_email: str):
    requests.post(
        f'{QB_BASE}/invoice/{invoice_id}/send',
        params={**PARAMS, 'sendTo': client_email},
        headers=HEADERS
    )
    # QuickBooks sends a standard email with a payment link

Refreshing OAuth Tokens

The QuickBooks access token expires after 60 minutes. The refresh token expires after 100 days. It is important to implement auto-refresh:

def refresh_access_token(refresh_token: str) -> dict:
    import base64
    credentials = base64.b64encode(
        f'{CLIENT_ID}:{CLIENT_SECRET}'.encode()
    ).decode()

    resp = requests.post(
         'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
        headers={
            'Authorization': f'Basic {credentials}',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        data={
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token
        }
    )
    return resp.json()  # access_token + refresh_token (rotating)

QuickBooks uses rotating refresh tokens — each refresh issues a new refresh token and invalidates the old one. This must be stored in the database, not in a config file.

Payment Webhook

QuickBooks webhooks are configured in the Intuit Developer Portal. The payload is minimal: only entity type, ID, and operation. Full details must be fetched separately:

from flask import Flask, request

app = Flask(__name__)

@app.route('/webhooks/quickbooks', methods=['POST'])
def qb_webhook():
    payload = request.json
    for notification in payload.get('eventNotifications', []):
        realm_id = notification['realmId']
        for entity in notification.get('dataChangeEvent', {}).get('entities', []):
            if entity['name'] == 'Payment' and entity['operation'] == 'Create':
                payment_id = entity['id']
                # Request payment details
                payment = get_payment_details(realm_id, payment_id)
                # Update Kommo by invoice_id from payment
                sync_payment_to_kommo(payment)
    return '', 200

Real-World Case

Consulting company (US market, 20–30 projects per quarter, clients in the US and Canada):

  • Before: after Won, the manager switched to QuickBooks, manually created the customer, generated the invoice, and sent the email. Average time from Won to invoice: 3–4 days.
  • After: Won in Kommo -> client receives QuickBooks invoice within 5 minutes -> payment status automatically updates in Kommo upon receipt of payment.
  • Additionally: the accountant stopped requesting data from managers for invoicing — everything comes from the CRM automatically.

A similar pattern using Zoho Books — there the REST API uses regional OAuth but without rotating refresh tokens. For the US market, QuickBooks is the standard choice.

Who This Is Relevant For

  • Clients primarily in the US and Canada — QuickBooks is the de facto standard
  • 10+ invoices per month currently created manually
  • Cycle: Won -> invoice -> payment -> next pipeline stage
  • Accountant and manager work in different systems and need synchronisation
  • QuickBooks Payments is used for online payments

Frequently Asked Questions

How complex is QuickBooks OAuth to maintain?

The main complexity is rotating refresh tokens: each time a token is refreshed, the new refresh token must be saved. If this is missed — the next refresh will use the old invalidated token and authorisation will break. In practice: store tokens in a database with a timestamp, refresh 5 minutes before the access token expires, and log every refresh.

Which Item should I use when creating an invoice?

An Item (product/service) in QuickBooks is a required object in a Line. You can create one universal Item “Consulting Services” via the QB UI and use its ID for all invoices from Kommo. Or create Items dynamically via POST /item — but using a fixed Item for the integration is simpler.

Are there QuickBooks API rate limits?

Yes. 500 requests per minute for a real OAuth app. For a typical Kommo volume (up to 100 deals per month), limits are irrelevant. The webhook payload is minimal — each notification requires an additional GET request, which should be considered at high volume.

How do I test with the QuickBooks Sandbox?

Intuit provides a sandbox environment: sandbox-quickbooks.api.intuit.com. A sandbox company is created automatically when registering in the Intuit Developer Portal. Webhook testing — via ngrok or similar tools (Intuit cannot send webhooks to localhost).

Is QuickBooks Plus required for the API?

The API is available on all plans, including Simple Start ($35/month). Multi-currency requires Plus and above. For the US market without multi-currency, Simple Start is sufficient.

Summary

  • QuickBooks Online REST API v3: OAuth 2.0 with rotating refresh tokens, realmId in every request
  • Search Customer via query API, create invoice with Line items, send via /send endpoint
  • Webhook on Payment -> automatic stage update in Kommo
  • Rotating refresh tokens — the key architectural consideration, requires storage in the database
  • Typical development time: 2–3 weeks

If you work with QuickBooks and Kommo and want to automate invoice generation — describe your pricing structure and payment workflow. Exceltic.dev will analyse the mapping and propose an architecture.

More articles

All →