Discuss your task

Kommo + Google Sheets: Two-Way Sync Without Zapier

Kommo + Google Sheets: Two-Way Sync Without Zapier

You can build two-way synchronization between Kommo and Google Sheets without Zapier through a direct integration: a Kommo webhook writes deal changes to the Sheet in real time, while a periodic script reads changes from the Sheet and updates deals via the Kommo API. The key requirement for correct operation is upsert by deal ID - not a blind append - and protection against race conditions when both sides change data at the same time.

The native Kommo integration with Google Sheets and Zapier each solve only half the problem - and specifically not the half most teams actually need. Below is an architecture that works in both directions.

In Exceltic.dev projects we regularly see the same pattern: Google Sheets lives alongside the CRM as the team’s “working spreadsheet.” Sales managers enter data in the Sheet, the manager edits the pipeline in Kommo, and once a week someone spends two or three hours on manual reconciliation. In our observation, roughly 80% of companies on Kommo have at least one active Sheet that duplicates part of the CRM data. This is not a pathology - it is an adaptation to the actual workflow. The goal of the integration is not to eliminate the Sheet but to ensure data in it and in the CRM never diverge.

This article walks through the architecture of two-way sync without intermediaries: what specifically breaks in the native solution and in Zapier, how the correct setup works, and what numbers it delivers on a real project.

Why the Native Kommo Integration with Sheets Does Not Solve the Problem

The native Kommo integration with Google Sheets is one-directional. It does exactly one thing: when a new row appears in the Sheet, it creates a lead in Kommo. Not a deal - a lead. Without custom fields, without linking to an existing deal, without updating records that already exist.

According to Kommo’s official documentation, the native integration has additional constraints:

  • processing stops at the first empty row in the spreadsheet
  • changing column headers after setup breaks the integration
  • only one sheet can be connected per integration

The main limitation: there is no sync in the reverse direction. When a manager changes a deal status in Kommo, nothing changes in the Sheet. When someone edits a row in the Sheet, Kommo does not know about it. The native integration is not synchronization - it is a one-time import.

Why Zapier Does Not Work Here

Zapier seems like the obvious solution, but in practice it creates three systemic problems that cannot be fixed through configuration.

Duplicate rows due to the absence of idempotency. When the leads.update trigger fires from Kommo, Zapier calls spreadsheets.values.append - which always adds a new row. If one deal was updated five times in a day, five rows with the same deal ID appear in the Sheet. Zapier cannot search for an existing row by key and update it - it can only append. The official workaround from the Zapier Community is to add a “Find or Create Row” step to a separate sheet plus a Filter step, which triples the number of API calls and turns a simple zap into a four-to-five-step construction.

Google Sheets API limits. Google Sheets API v4 has strict quotas: 300 requests per minute per project and 60 requests per minute per user. Important: Zapier uses a shared Google project for all its users. Under heavy load on the platform you receive 429 Too Many Requests not because of your own traffic but because of neighbors on the Zapier infrastructure. This is confirmed by threads in the Zapier Community - users see “Quota exceeded for quota group” even at low volumes of their own zap requests.

No race condition control. In a two-way scheme through Zapier the following scenario is possible: a manager changes a deal in Kommo -> Zapier writes to the Sheet -> someone changes the same row in the Sheet -> Zapier reads the Sheet -> creates a new record in Kommo. The result is that one deal multiplies or gets overwritten in an unpredictable way. Zapier has no locking or versioning mechanism to prevent such conflicts.

Term: Idempotency - the property of an operation whereby repeating the call with the same parameters produces the same result as the first call. A correct write to the Sheet must be idempotent: if a deal with ID 12345 already exists in the spreadsheet, update the existing row rather than adding a new one.

What a Custom Integration Delivers

Two-Way Sync Architecture

The architecture consists of two independent channels.

Channel 1: Kommo -> Sheet (webhook-driven, real time). Kommo sends a webhook on every leads.update and leads.add event. The webhook receiver (a microservice or serverless function) receives the payload with deal fields: id, name, status_id, price, custom_fields, responsible_user_id. Before writing to the Sheet the receiver checks whether a row with that id exists via spreadsheets.values.get. If the row is found, it executes spreadsheets.values.batchUpdate targeting that range. If it is not found, it calls spreadsheets.values.append with insertDataOption=INSERT_ROWS. This is the upsert logic at the Sheets API level.

Channel 2: Sheet -> Kommo (polling, every N minutes). A periodic script reads the entire data range via spreadsheets.values.get. It compares the last-change timestamp of each row (the service column last_modified_local) with the timestamp of the last sync. Rows changed after the last sync are sent to Kommo via PATCH /api/v4/leads/{id} with the corresponding fields. Rows without changes are skipped.

Upsert by Deal ID

The key to the whole scheme is the deal ID column as the table’s primary key. For any write to the Sheet a lookup is performed for the row with the needed ID via spreadsheets.values.get on the range of the ID column (for example Sheet1!A:A). If the ID is found, the row number is calculated and a targeted batchUpdate is executed at address Sheet1!A{row}:{lastCol}{row}. This approach eliminates duplicates regardless of how many times a deal is updated.

Race Condition Protection

To prevent conflicts when both sides change data at the same time, a sync_source field is used in each Sheet row. When writing from Kommo the value is set to crm. When a row is edited manually from the Sheet it is set to sheet. The polling channel script ignores rows where sync_source=crm and the change timestamp matches the last update from the webhook - this means the change already came from the CRM and does not need to be synced back. Additionally, a global lock is used: a semaphore cell (Sheet1!Z1) holds the flag sync_in_progress. The webhook receiver sets the flag before writing and clears it after. The polling script checks the flag before starting and postpones its run if the flag is active.

Step-by-Step Architecture

Step 1: Setting Up a Google Service Account

To work with Google Sheets API v4 without an OAuth flow tied to a specific user, a service account is used. A service account is created in Google Cloud Console and a JSON key is generated. The target Google Sheet is shared for editing with the service account email (in the form [email protected]). After this the integration runs autonomously without depending on any individual employee’s token.

Key Sheets API methods used in this integration:

  • spreadsheets.values.get - reading a range to search for a row by ID
  • spreadsheets.values.append - adding a new row (only for new deals)
  • spreadsheets.values.batchUpdate - updating existing rows by an exact range

Step 2: Configuring the Kommo Webhook

In Kommo settings (the “Webhooks” section), the webhook receiver endpoint is registered. Subscriptions are set to the events: leads.add, leads.update, leads.status. Kommo delivers deal fields in the payload including custom fields (custom_fields) with their IDs and values. For custom fields the webhook uses numeric IDs, not names - the mapping of IDs to readable Sheet column names is stored in the integration config.

Step 3: The Webhook Receiver

A microservice in any language (Python, Node.js) receives the POST request from Kommo, verifies the signature, extracts the deal data, and performs an upsert in the Sheet. Important: the response to the webhook must be sent within 10 seconds, otherwise Kommo will retry. To protect against duplicates on retries - a cache of processed webhook IDs (Redis or a simple file) for 60 seconds.

Step 4: Polling Script for Sheet -> Kommo

A cron job at a frequency that fits the team’s workflow: typically every 5-15 minutes. The script reads the entire Sheet, filters rows where last_modified_local is newer than last_sync_timestamp, and for each such row sends PATCH /api/v4/leads/{id} to the Kommo API. The result is written back to service columns: sync_status (success/error), last_synced_at.

Step 5: Table Structure

Required service columns, hidden from users:

  • deal_id - primary key, the deal ID in Kommo
  • last_modified_local - timestamp of the last manual edit (updated via an Apps Script onEdit trigger)
  • sync_source - crm or sheet
  • sync_status - status of the last sync
  • last_synced_at - timestamp of the last successful sync

User-facing columns: deal name, stage, amount, responsible person, custom fields as required by the project.

Real Case With Numbers

The company was a B2B distributor with a sales team of 8 people. Google Sheets was used as the primary working document for shipment planning: managers entered delivery dates and warehouse contact details directly in the spreadsheet. Kommo was used for pipeline management and communication. Every Friday the operations manager spent about 3 hours manually reconciling data between the Sheet and the CRM.

After implementing the custom two-way sync:

  • Manual weekly reconciliation: 0 hours (fully automated)
  • Sheet -> Kommo sync delay: up to 10 minutes (polling every 10 minutes)
  • Kommo -> Sheet sync delay: seconds (webhook in real time)
  • Data discrepancies between systems in the first 3 months of operation: 0
  • Duplicate rows in the Sheet: 0 (upsert by deal ID)

Implementation scope: approximately 3 working days including testing on a staging environment and onboarding the team to the service columns.

For comparison: the previous attempt to configure sync via Zapier lasted two weeks before it was disabled due to a growing number of duplicates and unpredictable behavior on simultaneous edits.

Who This Approach Is Right For - and When to Leave Sheets Behind Entirely

The approach makes sense if:

  • Google Sheets is a real working tool for the team (planning, logistics, operations) and not just an export for reporting
  • 1-3 columns in the Sheet are edited manually and need to reach the CRM
  • The team is not ready to move entirely into Kommo for all operations
  • The volume of active deals is up to 5,000 at a time (above that, it is worth looking at a full ETL)

When to remove Sheets entirely:

  • If Sheets is used only for reporting - replace it with a dashboard from CRM data. BI tools with a direct connection to Kommo or analytics platforms like Prooflytics work well here, providing a unified view of deals and marketing without manual exports.
  • If the Sheet has more than 10 columns of business data that all need to be in the CRM - this is a signal to reconsider the data model in Kommo and configure custom fields rather than maintaining a parallel spreadsheet.
  • If the cost of maintaining the sync is comparable to the cost of restructuring the process - do the restructuring.

If the company has outgrown Kommo for analytics and reporting but the Sheet workflow is still relevant, Kommo + n8n integration provides a more flexible platform for orchestrating such data flows with a visual editor and a self-hosted mode.

Frequently Asked Questions

Can the native Kommo integration with Google Sheets be used for two-way sync?

No. The native Kommo integration with Google Sheets works in one direction only: a new row in the Sheet creates a lead in Kommo. Reverse sync - changes to deals from Kommo back into the Sheet - is not supported by the native integration. In addition, the native integration cannot update existing records: every trigger creates a new lead rather than updating an existing one. Two-way sync with upsert logic requires a custom integration via Kommo webhooks and the Google Sheets API v4 directly.

Why does Zapier create duplicate rows in Google Sheets?

When Zapier fires on a Kommo event it uses the spreadsheets.values.append method, which always adds a new row - it does not check whether a row with that deal ID already exists. If a deal is updated multiple times, each update adds a new row. The Zapier Community offers a workaround using “Find or Create Row” plus a Filter step, but this does not fix the problem architecturally - it only reduces the frequency of duplicates at the cost of tripling the number of API calls. The correct solution is to implement upsert in code: first find the row by deal ID via spreadsheets.values.get, then update it via batchUpdate or add it via append only if the row is not found.

How quickly does data synchronize?

It depends on the direction. Kommo -> Sheet: sync happens in real time via webhook - the delay is seconds (the time for the webhook receiver to process the request). Sheet -> Kommo: sync happens on the polling script schedule, typically every 5-15 minutes. This is sufficient for most operational scenarios where data in the Sheet is edited as part of a workflow rather than in real time. If you need immediate Sheet -> Kommo sync, you can add a Google Apps Script onEdit trigger that calls the webhook receiver immediately when a cell is saved.

How are conflicts handled during simultaneous edits?

With the right architecture, conflicts are handled through source priority and a semaphore. If a manager changes a deal in Kommo and someone simultaneously edits the same row in the Sheet, the webhook from Kommo writes the change to the Sheet with the tag sync_source=crm. The polling script sees this tag and does not send the record back to Kommo - otherwise an infinite sync loop would result. Additionally, a semaphore flag in a dedicated cell blocks polling while an active webhook write is in progress. This is not 100% protection against all edge cases, but it covers 99% of real-world usage scenarios.

Which Google Sheets API method should you use: append or batchUpdate?

It depends on the operation. spreadsheets.values.append - only for adding new rows (new deals that do not yet exist in the Sheet). spreadsheets.values.batchUpdate - for updating existing rows by an exact range (for example, Sheet1!B5:H5). Never use append to update existing records - this is the primary cause of duplicates when integrating through Zapier and other no-code tools. The rule: always search for the row by deal ID via spreadsheets.values.get first, and only based on the search result choose the write method.


If you have a Google Sheet that lives alongside Kommo and regularly requires manual reconciliation - describe the task to the Exceltic.dev team. We will identify which columns need to sync in which direction and estimate the scope of work for a custom integration.

More articles

All →