Kommo + QuickBooks: автоматические инвойсы из воронки продаж

Kommo + QuickBooks: автоматические инвойсы из воронки продаж

QuickBooks Online — доминирующая бухгалтерская платформа в США, Канаде и ряде EU-рынков. REST API QuickBooks (Intuit API v3) поддерживает создание клиентов, инвойсов, получение статусов оплат и webhook-уведомления. Связка с Kommo закрывает стандартный паттерн: сделка Won -> Customer в QuickBooks -> Invoice -> после оплаты CRM обновляется, без ручного копирования данных.

QuickBooks vs Zoho Books vs Wave: когда QuickBooks

ПараметрQuickBooks OnlineZoho BooksWave
APIREST v3RESTGraphQL
Доминирующий рынокСША, КанадаГлобальноСША, Канада
Цена (Simple Start)$35/месБесплатно до $50kБесплатно (базовый)
МультивалютностьPlus и вышеВсе тарифыЧастично
Встроенный эквайрингДа (QuickBooks Payments)ДаСША/Канада
Экосистема750+ интеграцийZoho 50+ продуктовНезависимый

QuickBooks выбирают компании, работающие с US/CA клиентами или аудиторами — это де-факто стандарт в Северной Америке. Сравнение с Zoho Books и Wave — в отдельных статьях.

Что синхронизируется

Kommo -> QuickBooks:
— Контакт сделки -> Customer в QB (дедупликация по email через query API)
— Название и сумма сделки -> строки Invoice
— Срок оплаты из кастомного поля -> Terms (Net 15, Net 30 и т.д.)
— ID инвойса и ссылка -> кастомные поля в карточке Kommo

QuickBooks -> Kommo:
— Webhook Payment с txnStatus = Completed -> поле «Оплачено» в сделке
— Перевод сделки на этап «Оплата получена»
— Дата и сумма оплаты -> Note на сделке

Архитектура

Kommo Webhook: сделка Won
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> имя, email, сумма, срок оплаты
  2. QB API: POST /v3/company/{realmId}/query
     -> SELECT * FROM Customer WHERE PrimaryEmailAddr = '{email}'
     -> найден: использовать Id
     -> не найден: POST /v3/company/{realmId}/customer
  3. QB API: POST /v3/company/{realmId}/invoice
     -> CustomerRef + Line items + DueDate
     -> получить Id, DocNumber, InvoiceLink
  4. QB API: POST /v3/company/{realmId}/invoice/{id}/send
     -> отправить инвойс клиенту по email
  5. Kommo: PATCH /leads/{id}
     -> обновить поля qb_invoice_id, invoice_url

QuickBooks Webhook: Payment (txnStatus = Completed)
  ↓ Backend
  1. GET /v3/company/{realmId}/payment/{paymentId}
     -> найти LinkedTxn с Invoice Id
  2. GET хранилища: найти kommo_deal_id по qb_invoice_id
  3. Kommo: PATCH /leads/{deal_id}
     -> этап -> «Оплачено», поле payment_date

QuickBooks REST API: ключевые запросы

QuickBooks API использует OAuth 2.0 (Authorization Code Flow). Все запросы идут на https://quickbooks.api.intuit.com/v3/company/{realmId}/. Обязательный параметр: ?minorversion=75.

Поиск или создание 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:
    # Поиск по 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']

    # Создание нового клиента
    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']

Создание инвойса:

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 из 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']}

Отправка инвойса клиенту:

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 отправляет стандартное письмо с ссылкой на оплату

Обновление OAuth-токена

Access token QuickBooks истекает через 60 минут. Refresh token — через 100 дней. Важно реализовать автообновление:

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 использует rotating refresh tokens — при каждом обновлении выдаётся новый refresh token, старый инвалидируется. Это важно хранить в БД, а не в конфиге.

Webhook на оплату

QuickBooks Webhooks настраиваются в Intuit Developer Portal. Payload — минималистичный: только entity type, ID и операция. Полные данные нужно запрашивать отдельно:

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']
                # Запрос деталей платежа
                payment = get_payment_details(realm_id, payment_id)
                # Обновить Kommo по invoice_id из payment
                sync_payment_to_kommo(payment)
    return '', 200

Реальный кейс

Консалтинговая компания (US-рынок, 20–30 проектов в квартал, клиенты в США и Канаде):

  • До: менеджер после Won переключался в QuickBooks, вручную создавал клиента, формировал инвойс, отправлял письмо. Среднее время от Won до выставления счёта — 3–4 дня.
  • После: Won в Kommo -> через 5 минут клиент получает инвойс из QuickBooks -> статус оплаты автоматически обновляется в Kommo при получении платежа.
  • Дополнительно: бухгалтер перестал запрашивать у менеджеров данные для выставления счётов — всё приходит из CRM автоматически.

Похожий паттерн, но через Zoho Books — там REST API с региональным OAuth, но без rotating refresh tokens. Для US-рынка QuickBooks — стандартный выбор.

Для кого актуально

  • Клиенты преимущественно в США, Канаде — QuickBooks де-факто стандарт
  • 10+ инвойсов в месяц, которые сейчас создаются вручную
  • Цикл: Won -> инвойс -> оплата -> следующий этап воронки
  • Бухгалтер и менеджер работают в разных системах, нужна синхронизация
  • Используется QuickBooks Payments для онлайн-оплат

Часто задаваемые вопросы

QuickBooks OAuth — насколько сложно поддерживать?

Glавная сложность — rotating refresh tokens: при каждом обновлении токена нужно сохранить новый refresh token. Если пропустить это — при следующем обновлении будет использован старый инвалидированный токен и авторизация сломается. На практике: хранить токены в БД с timestamp, обновлять за 5 минут до истечения access token, логировать каждый refresh.

Какой Item использовать при создании инвойса?

Item (товар/услуга) в QuickBooks — обязательный объект в Line. Можно создать один универсальный Item «Consulting Services» через QB UI и использовать его ID для всех инвойсов из Kommo. Или создавать Item динамически через POST /item — но проще использовать фиксированный для интеграции.

Есть ли лимиты QuickBooks API?

Да. 500 запросов в минуту на реаl OAuth app. Для типового объёма Kommo (до 100 сделок в месяц) лимиты нерелевантны. Webhook payload минималистичен — за каждым уведомлением нужен дополнительный GET-запрос, что нужно учитывать при высоком объёме.

QuickBooks Sandbox — как тестировать?

Intuit предоставляет Sandbox-среду: sandbox-quickbooks.api.intuit.com. Sandbox-компания создаётся автоматически при регистрации в Intuit Developer Portal. Webhook-тестирование — через ngrok или аналоги (Intuit не может отправить webhook на localhost).

Нужен ли QuickBooks Plus для API?

API доступен на всех тарифах, включая Simple Start ($35/мес). Мультивалютность — только на Plus и выше. Для US-рынка без мультивалюты Simple Start достаточен.

Итого

  • QuickBooks Online REST API v3: OAuth 2.0 с rotating refresh tokens, realmId в каждом запросе
  • Поиск Customer через query API, создание инвойса с Line items, отправка через /send endpoint
  • Webhook на Payment -> автообновление этапа в Kommo
  • Rotating refresh tokens — ключевой архитектурный нюанс, требует хранения в БД
  • Типовой срок разработки — 2–3 недели

Если вы работаете на QuickBooks и Kommo и хотите автоматизировать выставление инвойсов — опишите вашу структуру ценообразования и схему оплат. Exceltic.dev разберёт маппинг и предложит архитектуру.

Ещё статьи

Все →