Kommo + Stripe: Payment Events in the Deal Card via Webhook
When a client pays an invoice through Stripe, your CRM does not find out automatically - unless you have set up webhook event handling. The custom flow looks like this: Stripe sends payment_intent.succeeded to your endpoint, the server verifies the signature via the Stripe-Signature header, extracts kommo_lead_id from the payment metadata, and updates the deal’s custom fields in Kommo via PATCH /api/v4/leads.
The result: the sales rep sees the “Paid” status, date, and amount right in the deal card - without ever opening the Stripe Dashboard.
Exceltic.dev has delivered several projects using this integration pattern for B2B companies that accept payments through Stripe in USD and EUR. The pattern is always the same: financial data lives in Stripe, deals live in Kommo, and there is no automatic channel between them. Sales reps either mark payment manually in the CRM, or the finance team reconciles the Stripe statement against the deal list at the end of the month. This article explains how to close that gap with a direct webhook integration.
Why the Native Options Fall Short
The Stripe App Marketplace includes several CRM connectors, but Kommo is not among them. The Kommo Marketplace, in turn, offers payment widgets for CIS-market gateways, but not for Stripe.
Tools like Zapier or Make offer a partial solution: you can set up a trigger on a Stripe webhook and an action to update Kommo. The problem is in the details:
- Zapier accepts Stripe webhooks via polling or a webhook trigger, but does not verify
Stripe-Signature- an event from any source will be processed as genuine. - Under load or during an outage, Zapier silently drops events without alerts. For payment data, this is critical.
- Updating a specific custom field in Kommo via Zapier requires knowing the
field_id, which cannot be selected from a dropdown - you have to manually enter the numeric ID. - Idempotency (protection against double-writes when a webhook is delivered more than once) is not implemented in Zapier.
For one-off testing, Zapier is fine. For production processing of payment events, it is not.
What Gets Built
Stripe webhook -> server -> signature check -> write to Kommo
Webhook secret and Stripe-Signature
Stripe-Signature is an HTTP header that Stripe adds to every webhook request. It contains a timestamp and an HMAC-SHA256 signature computed from the request body and your endpoint secret (whsec_...). Verifying the signature guarantees the request came from Stripe and not from an outside source.
Critical requirement: verification requires the raw request body (raw body). If your framework parses JSON before verification, the signature will not match. In Python/Flask this is request.data; in Node.js/Express it is express.raw({ type: 'application/json' }).
Verification example in 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) # signature mismatch - reject
if event["type"] == "payment_intent.succeeded":
pi = event["data"]["object"]
lead_id = pi["metadata"].get("kommo_lead_id")
amount = pi["amount"] # in smallest units (cents)
currency = pi["currency"]
pi_id = pi["id"] # for idempotency
update_kommo_lead(lead_id, amount, currency, pi_id)
return "", 200
Which events to handle
Three core Stripe events for payment tracking:
payment_intent.succeeded- a one-time payment completed successfully. The object containsid,amount(in cents),currency,metadata,customer.invoice.paid- a subscription or invoice was paid. The object containsamount_paid,subscription,payment_intent. Fires on every successful subscription charge.checkout.session.completed- the customer finished payment through Stripe Checkout. Containsamount_total,payment_intent,client_reference_id- a convenient field for passingkommo_lead_idwithout needing metadata.
For most B2B scenarios payment_intent.succeeded is sufficient. invoice.paid is needed if you work with subscriptions through Stripe Billing.
Idempotency via payment_intent.id
Stripe guarantees at-least-once webhook delivery - a single event can arrive twice on timeouts or restarts. Without duplicate protection, the payment amount gets written to Kommo twice.
Solution: store payment_intent.id in a custom deal field. Before writing, check whether the field already contains this ID; if it does, skip processing.
def update_kommo_lead(lead_id, amount, currency, pi_id):
# Check idempotency
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 # already processed
# Update fields in 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}]}, # convert cents
{"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)
# Add a note to the deal card
note_text = f"Stripe: paid {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}
}])
Custom deal fields in Kommo
The integration requires creating several fields on the deal entity:
| Field | Type | What it stores |
|---|---|---|
| Payment status | List | pending / paid / failed / refunded |
| Amount paid | Number | Amount in base units (not cents) |
| Currency | Text | USD, EUR, GBP |
| Stripe Payment Intent ID | Text | For idempotency |
| Payment date | Date/time | Unix timestamp from the event |
The field_id for each field is retrieved via GET /api/v4/leads/custom_fields - it is a numeric identifier used in custom_fields_values.
Passing kommo_lead_id to Stripe
To know which deal a payment belongs to when the webhook arrives, the lead_id must be passed to Stripe when creating the payment intent:
payment_intent = stripe.PaymentIntent.create(
amount=amount_in_cents,
currency="usd",
metadata={
"kommo_lead_id": str(lead_id),
"kommo_contact_id": str(contact_id),
}
)
Alternative for Stripe Checkout: use client_reference_id when creating the session - this field is available in checkout.session.completed without needing metadata.
Step-by-Step Flow
- When creating a PaymentIntent in Stripe - pass
kommo_lead_idinmetadata. - Stripe sends
payment_intent.succeededto your endpoint on successful payment. - The server reads the raw body and extracts the
Stripe-Signatureheader. stripe.Webhook.construct_event()verifies the signature via HMAC-SHA256. On error - return400, log it.- The server returns
200 OKimmediately (before processing) - otherwise Stripe will retry after 5 seconds. - In the background: read
kommo_lead_idfromevent.data.object.metadata. - Check
payment_intent.idin the Kommo deal’s custom field - idempotency check. PATCH /api/v4/leads- update status, amount, currency, PI ID, date.POST /api/v4/leads/notes- add a note with the amount and Stripe ID.- Optionally: move the deal stage to “Paid” via
status_idin the same PATCH.
Handling invoice.paid for subscriptions follows the same pattern, but kommo_lead_id is taken from event.data.object.metadata through the associated payment_intent.
Real-World Case
A B2B agency with 8 sales reps, average deal size $3,000-$12,000, payments via Stripe in USD. Deal cycle of 2-6 weeks with payment at the end.
The problem before integration: the finance manager manually reconciled the Stripe Dashboard against the Kommo deal list every Monday - roughly 2 hours per week. Sales reps did not know about received payments until that reconciliation and could not move on to the next stage (onboarding, handoff to the delivery team) in time.
What was built: a webhook endpoint on Python/FastAPI, handling of payment_intent.succeeded and invoice.paid, writing to 5 custom deal fields, a note with amount and Stripe ID, and automatic deal stage transition to “Paid” on success.
Results one month after launch:
- Manual reconciliation: 0 minutes per week (the finance manager checks the report in Kommo, not Stripe).
- Average time from payment to onboarding start: from 4 business days down to 2 hours (the sales rep gets a stage-change notification immediately).
- First month: 0 double-write incidents (idempotency via PI ID works).
Project delivery time: 2 weeks including testing in Stripe test mode.
Who This Is For
This pattern is relevant for companies that:
- Manage deals in Kommo and accept payment through Stripe in foreign currency.
- Have a deal cycle longer than 1 week - the sales rep cannot track payment status from memory.
- Have role separation: a sales rep and a finance/accounting person work with different tools.
- Use subscriptions or recurring payments through Stripe Billing - every charge must be reflected in the deal.
If you have already set up custom integrations for Kommo and want to add a payment layer, this is a natural next step. An overview of the platform and its API model is covered in the Kommo CRM review.
For companies that invoice through Stripe and want to set up an automatic Kommo pipeline around the payment cycle, the topics are connected: pipeline stages determine when a PaymentIntent is created and how its webhook is handled.
Term: A webhook endpoint is a URL on your server to which Stripe sends HTTP POST requests when payment events occur (payment, refund, failure). Unlike polling, a webhook does not require periodic API calls - data arrives at the moment the event happens.
Frequently Asked Questions
How reliable is Stripe webhook delivery?
Stripe guarantees at-least-once delivery: if your endpoint does not return 2xx, Stripe retries with exponential backoff - for up to 3 days. In practice 99.9%+ of events are delivered on the first attempt. The key condition: the endpoint returns 200 OK immediately, before processing completes. To achieve this, move processing to a queue (Celery, RQ, Bull) - the endpoint only receives and enqueues, it does not process synchronously. You can inspect delivery history and retry manually via Stripe Workbench -> Events.
For invoice.paid on a subscription - update the same deal or create a new one?
It depends on your model. For monthly retainers, the typical pattern is: one deal per contract, each invoice.paid adds a note with the amount and date, and the “Last payment” field is updated. The total contract value is summed in a custom field or in a dashboard. If each billing period is a separate project, a new deal is created via POST /api/v4/leads on the first invoice.paid of that period.
How do I pass kommo_lead_id to Stripe if the payment is created on the client side?
If the client pays through Stripe Checkout (hosted page), pass kommo_lead_id in client_reference_id when creating the Session on the server. This field cannot be modified from the client side and will be present in checkout.session.completed. For a PaymentIntent created on the client side via Stripe.js - metadata is not available until the server confirms it. The correct pattern: the server creates the PaymentIntent with metadata, the client only confirms using the client_secret.
How do I handle a refund from Stripe?
Stripe sends charge.refunded on a full or partial refund. The object contains amount_refunded and payment_intent. Use payment_intent.id to find the deal (via the idempotency field), then update the status to “Refunded” and record the refund amount. For partial refunds - a separate custom field or a note with a breakdown. The sales rep sees the full payment history in the deal card without opening Stripe.
Do I need a dedicated server, or will serverless work?
Serverless (AWS Lambda, Vercel Functions, Cloudflare Workers) is fully suitable for receiving Stripe webhooks. The only requirement: the function must return 200 OK within Stripe’s timeout (about 30 seconds). If processing takes longer - accept the webhook in serverless, queue the task (SQS, Upstash), and process it asynchronously. For an MVP handling up to 100 payments per day the difference is negligible and serverless is simpler to deploy.
Summary
- There is no native Kommo + Stripe integration - and there will not be one without custom development.
- Zapier/Make do not verify
Stripe-Signatureand do not protect against duplicates - they are not suitable for production payment event processing. - Three events to handle:
payment_intent.succeeded(one-time payments),invoice.paid(subscriptions),checkout.session.completed(Checkout flow). - Idempotency via
payment_intent.idin a custom field is mandatory - Stripe delivers events more than once. - Kommo API:
PATCH /api/v4/leadsfor fields +POST /api/v4/leads/notesfor the note. - Webhook event documentation: stripe.com/docs/webhooks. Kommo API documentation: developers.kommo.com.
If you accept payments through Stripe and manage deals in Kommo - describe your setup (payment types, deal volume, which fields matter) to the Exceltic.dev team. We will review the architecture and estimate the scope of work.