Обсудить задачу

Kommo + Stripe: платёжные события в карточку сделки через webhook

Когда клиент оплачивает счёт через Stripe, CRM об этом не узнает автоматически - если только вы не настроили приём webhook-событий. Кастомная схема выглядит так: Stripe отправляет payment_intent.succeeded на ваш endpoint, сервер проверяет подпись через Stripe-Signature header, извлекает kommo_lead_id из метаданных платежа и обновляет кастомные поля сделки в Kommo через PATCH /api/v4/leads.

Результат: менеджер видит статус «Оплачено», дату и сумму прямо в карточке - без захода в Stripe Dashboard.

У Exceltic.dev несколько реализованных проектов такой интеграции для B2B-компаний, принимающих платежи через Stripe в USD и EUR. Паттерн всегда один: финансовые данные живут в Stripe, сделки - в Kommo, и между ними нет автоматического канала. Менеджеры либо вручную отмечают оплату в CRM, либо финансист сверяет выписку Stripe со списком сделок в конце месяца. Эта статья описывает, как закрыть этот разрыв через прямую webhook-интеграцию.

Почему нативного не хватает

Stripe App Marketplace содержит несколько CRM-коннекторов, но Kommo там нет. Kommo Marketplace, в свою очередь, предлагает виджеты для платёжных систем СНГ-рынка, но не для Stripe.

Apps вроде Zapier или Make дают частичное решение: можно настроить триггер на webhook от Stripe и action на обновление Kommo. Проблема в деталях:

  • Zapier принимает Stripe webhooks через polling или webhook trigger, но не верифицирует Stripe-Signature - событие от любого источника будет обработано как настоящее.
  • При нагрузке или сбое Zapier пропускает события без алертов. Для платёжных данных это критично.
  • Обновление конкретного кастомного поля в Kommo через Zapier требует знания field_id, который нельзя выбрать из выпадающего списка - нужен ручной ввод числового ID.
  • Идемпотентность (защита от двойной записи при повторной доставке webhook) в Zapier не реализована.

Для одноразового тестирования Zapier подходит. Для продакшн-обработки платёжных событий - нет.

Что реализуется

Stripe webhook -> сервер -> проверка подписи -> запись в Kommo

Webhook secret и Stripe-Signature

Stripe-Signature - это HTTP-заголовок, который Stripe добавляет к каждому webhook-запросу. Он содержит timestamp и HMAC-SHA256 подпись, вычисленную от тела запроса и секрета вашего endpoint (whsec_...). Проверка подписи гарантирует, что запрос пришёл от Stripe, а не от постороннего источника.

Критическое требование: для верификации нужно сырое тело запроса (raw body). Если фреймворк парсит JSON до верификации - подпись не совпадёт. В Python/Flask это request.data, в Node.js/Express - express.raw({ type: 'application/json' }).

Пример верификации на Python:

import stripe
from flask import Flask, request, abort

app = Flask(__name__)
webhook_secret = "whsec_your_secret_here"

@app.route("/stripe/webhook", methods=["POST"])
def stripe_webhook():
    payload = request.data          # raw bytes, not parsed JSON
    sig_header = request.headers.get("Stripe-Signature")

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, webhook_secret
        )
    except stripe.error.SignatureVerificationError:
        abort(400)                  # подпись не совпала - отклоняем

    if event["type"] == "payment_intent.succeeded":
        pi = event["data"]["object"]
        lead_id = pi["metadata"].get("kommo_lead_id")
        amount  = pi["amount"]      # в минимальных единицах (центы)
        currency = pi["currency"]
        pi_id   = pi["id"]          # для идемпотентности
        update_kommo_lead(lead_id, amount, currency, pi_id)

    return "", 200

Какие события обрабатываем

Три основных события Stripe для учёта платежей:

  • payment_intent.succeeded - разовый платёж успешно завершён. Объект содержит id, amount (в центах), currency, metadata, customer.
  • invoice.paid - подписка или инвойс оплачены. Объект содержит amount_paid, subscription, payment_intent. Срабатывает при каждом успешном списании по подписке.
  • checkout.session.completed - клиент завершил оплату через Stripe Checkout. Содержит amount_total, payment_intent, client_reference_id - удобное поле для передачи kommo_lead_id без метаданных.

Для большинства B2B-сценариев достаточно payment_intent.succeeded. invoice.paid нужен если вы работаете с подписками через Stripe Billing.

Идемпотентность через payment_intent.id

Stripe гарантирует доставку webhook at-least-once - одно событие может прийти дважды при таймаутах или перезапусках. Без защиты от дублей сумма оплаты запишется в Kommo дважды.

Решение: храним payment_intent.id в кастомном поле сделки. Перед записью проверяем - если поле уже содержит этот ID, обработку пропускаем.

def update_kommo_lead(lead_id, amount, currency, pi_id):
    # Проверяем идемпотентность
    lead = get_kommo_lead(lead_id)  # GET /api/v4/leads/{id}
    stored_pi_id = get_custom_field_value(lead, FIELD_ID_PAYMENT_INTENT)
    if stored_pi_id == pi_id:
        return  # уже обработано

    # Обновляем поля в Kommo
    patch_payload = [{
        "id": int(lead_id),
        "custom_fields_values": [
            {"field_id": FIELD_ID_PAYMENT_STATUS,
             "values": [{"value": "paid"}]},
            {"field_id": FIELD_ID_AMOUNT_PAID,
             "values": [{"value": amount / 100}]},   # конвертируем центы
            {"field_id": FIELD_ID_CURRENCY,
             "values": [{"value": currency.upper()}]},
            {"field_id": FIELD_ID_PAYMENT_INTENT,
             "values": [{"value": pi_id}]},
        ]
    }]
    # PATCH https://{subdomain}.kommo.com/api/v4/leads
    kommo_patch("/api/v4/leads", patch_payload)

    # Добавляем примечание в карточку
    note_text = f"Stripe: оплачено {amount / 100:.2f} {currency.upper()} | ID: {pi_id}"
    # POST https://{subdomain}.kommo.com/api/v4/leads/notes
    kommo_post(f"/api/v4/leads/notes", [{
        "entity_id": int(lead_id),
        "note_type": "common",
        "params": {"text": note_text}
    }])

Кастомные поля сделки в Kommo

Для схемы нужно создать несколько полей в типе сделки:

ПолеТипЧто хранит
Статус оплатыСписокpending / paid / failed / refunded
Сумма оплатыЧислоСумма в основных единицах (не центах)
ВалютаТекстUSD, EUR, GBP
Stripe Payment Intent IDТекстДля идемпотентности
Дата оплатыДата/времяUnix timestamp из события

field_id каждого поля получаем через GET /api/v4/leads/custom_fields - это числовой идентификатор, который используется в custom_fields_values.

Передача kommo_lead_id в Stripe

Чтобы при получении webhook знать, к какой сделке относится платёж, lead_id нужно передать в Stripe при создании платёжного намерения:

payment_intent = stripe.PaymentIntent.create(
    amount=amount_in_cents,
    currency="usd",
    metadata={
        "kommo_lead_id": str(lead_id),
        "kommo_contact_id": str(contact_id),
    }
)

Альтернатива для Stripe Checkout: client_reference_id при создании сессии - это поле доступно в checkout.session.completed без метаданных.

Пошаговая схема

  1. При создании PaymentIntent в Stripe - передаём kommo_lead_id в metadata.
  2. Stripe при успешной оплате отправляет payment_intent.succeeded на ваш endpoint.
  3. Сервер читает raw body, извлекает Stripe-Signature header.
  4. stripe.Webhook.construct_event() верифицирует подпись через HMAC-SHA256. Если ошибка - возвращаем 400, логируем.
  5. Сервер возвращает 200 OK немедленно (до обработки) - иначе Stripe повторит попытку через 5 секунд.
  6. В фоне: читаем kommo_lead_id из event.data.object.metadata.
  7. Проверяем payment_intent.id в кастомном поле сделки Kommo - идемпотентность.
  8. PATCH /api/v4/leads - обновляем статус, сумму, валюту, PI ID, дату.
  9. POST /api/v4/leads/notes - добавляем примечание с суммой и Stripe ID.
  10. Опционально: меняем этап сделки на «Оплачено» через status_id в том же PATCH.

Обработка invoice.paid для подписок аналогична, но kommo_lead_id берётся из event.data.object.metadata через associated payment_intent.

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

B2B-агентство, 8 менеджеров по продажам, средний чек - $3,000-12,000, оплата через Stripe в USD. Цикл сделки - 2-6 недель, оплата в конце.

Проблема до интеграции: финансист сверял Stripe Dashboard со списком сделок в Kommo вручную каждый понедельник - около 2 часов. Менеджеры не знали о поступившей оплате до этой сверки и не могли вовремя переходить к следующему этапу (онбординг, передача в команду).

Что реализовали: webhook-endpoint на Python/FastAPI, обработка payment_intent.succeeded и invoice.paid, запись в 5 кастомных полей сделки, примечание с суммой и Stripe ID, автоматический перевод сделки на этап «Оплачено» при успехе.

Результат через месяц после запуска:

  • Ручная сверка: 0 минут в неделю (финансист проверяет отчёт в Kommo, не Stripe).
  • Среднее время от оплаты до старта онбординга: с 4 рабочих дней до 2 часов (менеджер получает уведомление о смене этапа немедленно).
  • За первый месяц: 0 случаев двойной записи (идемпотентность через PI ID работает).

Срок реализации проекта - 2 недели, включая тестирование в Stripe test mode.

Для кого подходит

Схема актуальна для компаний, которые:

  • Ведут сделки в Kommo и принимают оплату через Stripe в иностранной валюте.
  • Цикл сделки больше 1 недели - менеджер не может держать статус оплаты в голове.
  • В команде есть разделение: менеджер по продажам и финансист/бухгалтер работают с разными инструментами.
  • Используют подписки или рекуррентные платежи через Stripe Billing - каждое списание должно отражаться в сделке.

Если вы уже настроили кастомные интеграции для Kommo и хотите добавить платёжный слой - это логичное следующее звено. Обзор возможностей платформы и её API-модели описан в обзоре Kommo CRM.

Для компаний, которые выставляют счета через Stripe и хотят настроить автоматическую воронку в Kommo под платёжный цикл - тема связана: этапы воронки определяют, когда создаётся PaymentIntent и как обрабатывается его webhook.

Термин: Webhook endpoint - это URL вашего сервера, на который Stripe отправляет HTTP POST-запросы при наступлении платёжных событий (оплата, возврат, сбой). В отличие от polling, webhook не требует периодических запросов к API - данные приходят в момент события.

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

Насколько надёжна доставка webhook от Stripe?

Stripe гарантирует at-least-once доставку: если ваш endpoint вернул не 2xx, Stripe повторяет попытку по экспоненциальному backoff - до 3 дней. На практике 99.9%+ событий доставляются при первой попытке. Ключевое условие: endpoint возвращает 200 OK немедленно, до завершения обработки. Для этого обработку выносят в очередь (Celery, RQ, Bull) - endpoint только принимает и ставит в очередь, не обрабатывает синхронно. Проверить доставку и повторить вручную можно через Stripe Workbench -> Events.

Что делать при invoice.paid для подписки - обновлять ту же сделку или создавать новую?

Зависит от модели. Для ежемесячных ретейнеров типичная схема: одна сделка на контракт, каждый invoice.paid добавляет примечание с суммой и датой, поле «Последний платёж» обновляется. Общая сумма по контракту суммируется на уровне кастомного поля или в дашборде. Если каждый период - отдельный проект, создаётся новая сделка через POST /api/v4/leads при первом invoice.paid периода.

Как передать kommo_lead_id в Stripe если оплата создаётся на стороне клиента?

Если клиент платит через Stripe Checkout (hosted page), kommo_lead_id передаётся в client_reference_id при создании Session на сервере. Это поле недоступно для изменения на клиенте и будет в checkout.session.completed. Для PaymentIntent, создаваемого на клиенте через Stripe.js, - metadata недоступны до подтверждения сервером. Корректная схема: сервер создаёт PaymentIntent с metadata, клиент только подтверждает через client_secret.

Как обрабатывать возврат через Stripe?

Stripe отправляет charge.refunded при полном или частичном возврате. Объект содержит amount_refunded и payment_intent. По payment_intent.id находим сделку (через поле идемпотентности), обновляем статус на «Возврат» и сумму возврата. Для частичных возвратов - отдельное кастомное поле или примечание с разбивкой. Менеджер видит историю платежей прямо в карточке без захода в Stripe.

Нужен ли выделенный сервер или подойдёт serverless?

Serverless (AWS Lambda, Vercel Functions, Cloudflare Workers) полностью подходит для приёма Stripe webhooks. Единственное условие: функция должна вернуть 200 OK в пределах таймаута Stripe (около 30 секунд). Если обработка длиннее - принять webhook в serverless, поставить задачу в очередь (SQS, Upstash), обработать асинхронно. Для MVP с объёмом до 100 платежей в день разница незначительна и serverless проще в деплое.

Итого

  • Нативной интеграции Kommo + Stripe нет - и не появится без кастомной разработки.
  • Zapier/Make не верифицируют Stripe-Signature и не защищают от дублей - не подходят для продакшн-обработки платёжных событий.
  • Три события для обработки: payment_intent.succeeded (разовые), invoice.paid (подписки), checkout.session.completed (Checkout flow).
  • Идемпотентность через payment_intent.id в кастомном поле - обязательна, Stripe доставляет события повторно.
  • Kommo API: PATCH /api/v4/leads для полей + POST /api/v4/leads/notes для примечания.
  • Документация по webhook-событиям: stripe.com/docs/webhooks. Документация по API Kommo: developers.kommo.com.

Если вы принимаете оплату через Stripe и ведёте сделки в Kommo - опишите вашу схему (типы платежей, объём сделок, какие поля важны) команде Exceltic.dev. Разберём архитектуру и оценим объём работ.


Title tag: Kommo + Stripe webhook: платежи в карточку сделки Meta description: Stripe знает об оплате сразу - Kommo нет. Настраиваем webhook: payment_intent.succeeded -> проверка подписи -> запись суммы и статуса в custom field сделки. LSI-запросы: stripe webhook kommo, payment_intent succeeded kommo, stripe signature verification, kommo custom fields api, kommo stripe интеграция без zapier Slug: /blog/kommo-stripe-webhooks-platezhi-v-sdelku

Ещё статьи

Все →