diff --git a/integrations/kommo/definitions/actions/contact.ts b/integrations/kommo/definitions/actions/contact.ts
new file mode 100644
index 00000000..b86c17e7
--- /dev/null
+++ b/integrations/kommo/definitions/actions/contact.ts
@@ -0,0 +1,59 @@
+import {z} from '@bpinternal/zui'
+import{ActionDefinition} from '@botpress/sdk'
+
+
+export const contactSchema = z.object({
+ id: z.number().describe('Contact ID'),
+ name: z.string().describe('Contact name'),
+ firstName: z.string().describe('First name'),
+ lastName: z.string().describe('Last name'),
+ responsibleUserId: z.number().describe('User responsible for this contact'),
+ groupId: z.number().describe('Group ID'),
+ updatedBy: z.number().describe('User who last updated this contact'),
+ createdAt: z.number().describe('When created (Unix timestamp)'),
+ updatedAt: z.number().describe('When last updated (Unix timestamp)'),
+ closestTaskAt: z.number().optional().describe('Closest task timestamp'),
+ isDeleted: z.boolean().describe('Whether contact is deleted'),
+ accountId: z.number().describe('Account ID'),
+})
+
+const createContact: ActionDefinition = {
+ title: 'Create Contact',
+ description: 'Creates a new contact',
+ input:{
+ schema:z.object({
+ name: z.string().optional().describe('Full contact name'),
+ firstName: z.string().optional().describe('First name'),
+ lastName: z.string().optional().describe('Last name'),
+ responsibleUserId: z.number().describe('User ID to assign this contact to'),
+ createdBy: z.number().describe('User ID who creates this contact'),
+ updatedBy: z.number().optional().describe('User ID who updates this contact'),
+ }),
+ },
+ output:{
+ schema:z.object({
+ contact: contactSchema
+ })
+ }
+
+}
+const searchContacts: ActionDefinition = {
+ title: "Search Contacts",
+ description: 'Search for contacts by name, phone number, or email',
+ input:{
+ schema: z.object({
+ query: z.string().describe('Search query (name, phone number, or email)')
+ }),
+ },
+ output: {
+ schema: z.object({
+ contacts: z.array(contactSchema).describe('Array of matching contacts (empty if none found)')
+ })
+
+ }
+}
+
+export const actions = {
+ createContact,
+ searchContacts,
+} as const
\ No newline at end of file
diff --git a/integrations/kommo/definitions/actions/index.ts b/integrations/kommo/definitions/actions/index.ts
new file mode 100644
index 00000000..9ec61ce3
--- /dev/null
+++ b/integrations/kommo/definitions/actions/index.ts
@@ -0,0 +1,8 @@
+import * as sdk from '@botpress/sdk'
+import { actions as leadActions } from './lead'
+import { actions as contactActions } from './contact'
+
+export const actions = {
+ ...leadActions,
+ ...contactActions,
+} as const satisfies sdk.IntegrationDefinitionProps['actions']
\ No newline at end of file
diff --git a/integrations/kommo/definitions/actions/lead.ts b/integrations/kommo/definitions/actions/lead.ts
new file mode 100644
index 00000000..bcc7c0ba
--- /dev/null
+++ b/integrations/kommo/definitions/actions/lead.ts
@@ -0,0 +1,77 @@
+import { z } from '@bpinternal/zui'
+import { ActionDefinition } from '@botpress/sdk'
+
+// Lead Schema: defines what a Kommo lead looks like in Botpress
+export const leadSchema = z.object({
+ id: z.number().describe('Lead ID'),
+ name: z.string().describe('Lead name'),
+ price: z.number().optional().describe('Lead value in dollars'),
+ responsibleUserId: z.number().optional().describe('User responsible for this lead'),
+ pipelineId: z.number().optional().describe('Which sales pipeline'),
+ statusId: z.number().optional().describe('Which stage in the pipeline'),
+ createdAt: z.number().describe('When created (Unix timestamp)'),
+ updatedAt: z.number().describe('When last updated (Unix timestamp)'),
+})
+
+
+const createLead: ActionDefinition = {
+ title: 'Create Lead',
+ description: 'Creates a new lead in Kommo CRM',
+ input: {
+ schema: z.object({
+ name: z.string().describe('Lead name (required)'),
+ price: z.number().optional().describe('Lead value in dollars'),
+ responsibleUserId: z.number().optional().describe('User ID to assign this lead to'),
+ pipelineId: z.number().optional().describe('Pipeline ID (defaults to main pipeline)'),
+ statusId: z.number().optional().describe('Initial status/stage ID'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ lead: leadSchema,
+ }),
+ },
+}
+
+
+const updateLead: ActionDefinition = {
+ title: 'Update Lead',
+ description: 'Updates an existing lead in Kommo',
+ input: {
+ schema: z.object({
+ leadId: z.number().describe('Lead ID to update'),
+ name: z.string().optional().describe('New name'),
+ price: z.number().optional().describe('New price'),
+ responsibleUserId: z.number().optional().describe('New responsible user'),
+ pipelineId: z.number().optional().describe('New pipeline ID'),
+ statusId: z.number().optional().describe('New status/stage ID'),
+ }),
+ },
+ output: {
+ schema: z.object({
+ lead: leadSchema,
+ }),
+ },
+}
+
+const searchLeads: ActionDefinition = {
+ title: 'Search Leads',
+ description: 'search for leads by name or other fields',
+ input:{
+ schema: z.object({
+ query: z.string().describe('Search query')
+ }),
+ },
+ output: {
+ schema: z.object({
+ leads: z.array(leadSchema).describe('Array of matching leads (empty if none found)')
+ })
+ }
+}
+
+// Export all lead actions
+export const actions = {
+ createLead,
+ searchLeads,
+ updateLead,
+} as const
diff --git a/integrations/kommo/definitions/index.ts b/integrations/kommo/definitions/index.ts
new file mode 100644
index 00000000..e964bf2a
--- /dev/null
+++ b/integrations/kommo/definitions/index.ts
@@ -0,0 +1,2 @@
+// Re-export everything from definitions
+export * from './actions'
diff --git a/integrations/kommo/hub.md b/integrations/kommo/hub.md
new file mode 100644
index 00000000..ec698743
--- /dev/null
+++ b/integrations/kommo/hub.md
@@ -0,0 +1,208 @@
+# Kommo Integration
+
+The Kommo integration allows you to connect your Botpress chatbot with Kommo. With this integration, your chatbot can create and update leads, manage contacts, and search through your CRM data.
+
+## Prerequisites
+
+Before using this integration, you need:
+
+1. A **Kommo account** with administrator access
+2. A **private integration** created in your Kommo account
+3. A **long-lived access token** from your private integration
+
+## Configuration
+
+To set up the Kommo integration in Botpress:
+
+### Step 1: Create a Private Integration in Kommo
+
+1. Log in to your Kommo account
+2. Navigate to **Settings** → **Integrations** → **Private Integrations**
+3. Click **Create Integration**
+4. Give your integration a name (e.g., "Botpress Chatbot")
+5. Set the required scopes:
+6. Save the integration
+
+### Step 2: Generate an Access Token
+
+1. In your private integration settings, click **Generate Token**
+2. Select the appropriate scopes
+3. Copy the **long-lived access token** - you'll need this for Botpress
+
+### Step 3: Configure Botpress Integration
+
+In your Botpress integration settings, provide:
+
+- **Base Domain**: Your Kommo subdomain (e.g., `yourcompany.kommo.com`)
+ - Do not include `https://` or trailing slashes
+ - Example: `mycompany.kommo.com`
+- **Access Token**: Paste the long-lived access token from Step 2
+
+## Available Actions
+
+### Create Lead
+
+Create a new lead in your Kommo CRM. Leads represent potential sales opportunities and can be tracked through your sales pipeline.
+
+**Required Fields:**
+
+- **Name**: Lead name or title (e.g., "Website Inquiry - John Doe")
+
+**Optional Fields:**
+
+- **Price**: Lead value in dollars (e.g., 5000)
+- **Responsible User ID**: Kommo user ID to assign this lead to
+- **Pipeline ID**: Which sales pipeline to add this lead to (defaults to main pipeline)
+- **Status ID**: Initial status/stage in the pipeline (e.g., "New Lead", "Contacted")
+
+**Output:** Returns the created lead with:
+- Lead ID
+- Name
+- Price
+- Responsible User ID
+- Pipeline ID
+- Status ID
+- Created and Updated timestamps
+
+**Example Use Cases:**
+- Capture leads from chatbot conversations
+- Create sales opportunities from customer inquiries
+- Track chatbot-generated leads in your CRM
+
+---
+
+### Update Lead
+
+Update an existing lead in Kommo. This action allows you to modify lead information as the conversation progresses or circumstances change.
+
+**Required Fields:**
+
+- **Lead ID**: The unique identifier of the lead to update
+
+**Optional Fields:**
+
+- **Name**: New lead name
+- **Price**: Updated lead value
+- **Responsible User ID**: Reassign to a different user
+- **Pipeline ID**: Move to a different pipeline
+- **Status ID**: Update the lead's stage (e.g., move from "Contacted" to "Qualified")
+
+**Output:** Returns the updated lead with all current information.
+
+**Example Use Cases:**
+- Update lead value based on customer responses
+- Move leads through your sales pipeline automatically
+- Reassign leads to different team members
+- Update lead information as you gather more details
+
+---
+
+### Search Leads
+
+Search for leads in your Kommo CRM using keywords. This action searches through lead names, prices, and other filled fields to find matching records.
+
+**Required Fields:**
+
+- **Query**: Search term (searches through lead names, prices, and other fields)
+
+**Output:** Returns an array of matching leads. Each lead includes:
+- Lead ID, Name, Price
+- Responsible User ID
+- Pipeline and Status IDs
+- Created and Updated timestamps
+
+**Example Use Cases:**
+- Look up existing leads by name before creating duplicates
+- Find leads by company name or keywords
+- Check if a lead already exists in your CRM
+- Search for leads by partial matches
+
+---
+
+### Create Contact
+
+Create a new contact in your Kommo CRM. Contacts represent individual people or entities in your CRM database.
+
+**Required Fields:**
+
+- **Responsible User ID**: Kommo user ID to assign this contact to
+- **Created By**: Kommo user ID of the person creating this contact
+
+**Optional Fields:**
+
+- **Name**: Full contact name
+- **First Name**: Contact's first name
+- **Last Name**: Contact's last name
+- **Updated By**: Kommo user ID of the person updating this contact
+
+**Output:** Returns the created contact with:
+- Contact ID
+- Name, First Name, Last Name
+- Responsible User ID
+- Group ID, Account ID
+- Created and Updated timestamps
+- Deletion status
+
+**Example Use Cases:**
+- Add new contacts from chatbot conversations
+- Create CRM entries for new customers
+- Store contact information collected during chat
+
+---
+
+### Search Contacts
+
+Search for contacts in your Kommo CRM by name, phone number, or email address. This action helps you find existing contacts before creating duplicates.
+
+**Required Fields:**
+
+- **Query**: Search term (can be name, phone number, or email)
+
+**Output:** Returns an array of matching contacts. Each contact includes:
+- Contact ID
+- Name, First Name, Last Name
+- Responsible User ID
+- Group ID, Account ID
+- Created and Updated timestamps
+- Deletion status
+
+If no contacts are found, returns an empty array.
+
+**Example Use Cases:**
+- Look up contacts by email before creating new ones
+- Find contacts by phone number
+- Search for contacts by name
+- Verify if a contact exists in your CRM
+
+## Common Workflows
+
+### Lead Qualification Flow
+
+1. **Search Contacts** to check if the person exists
+2. If not found, **Create Contact** with their information
+3. **Create Lead** for the sales opportunity
+4. As the conversation progresses, **Update Lead** with new information
+5. **Search Leads** to find related opportunities
+
+### Customer Inquiry Handling
+
+1. **Search Contacts** by email or phone
+2. If found, **Create Lead** and link to existing contact
+3. **Update Lead** status as you qualify the opportunity
+4. Assign lead to appropriate team member via **Update Lead**
+
+## Important Notes
+
+- **User IDs**: Responsible User IDs and Created By fields must be valid Kommo user IDs from your account
+- **Pipeline and Status IDs**: These are specific to your Kommo account setup. Check your Kommo settings for valid IDs
+- **Search**: Search queries are flexible and search across multiple fields
+- **Phone Numbers**: When searching contacts by phone, use the same format stored in Kommo
+- **Lead Values**: Price fields are in dollars (or your account's default currency)
+
+## Limitations
+
+- **Rate Limiting**: Kommo API has rate limits. Excessive requests may be throttled
+- **Field Validation**: Some fields (like User IDs, Pipeline IDs) must exist in your Kommo account
+- **Search Scope**: Searches are limited to fields populated in your CRM
+- **Permissions**: The access token must have appropriate scopes (`crm`, `notifications`)
+
diff --git a/integrations/kommo/icon.svg b/integrations/kommo/icon.svg
new file mode 100644
index 00000000..c7cb4c66
--- /dev/null
+++ b/integrations/kommo/icon.svg
@@ -0,0 +1,33 @@
+
+
+
diff --git a/integrations/kommo/integration.definition.ts b/integrations/kommo/integration.definition.ts
new file mode 100644
index 00000000..98802b42
--- /dev/null
+++ b/integrations/kommo/integration.definition.ts
@@ -0,0 +1,24 @@
+import { z, IntegrationDefinition } from '@botpress/sdk'
+import { integrationName } from './package.json'
+import { actions } from './definitions'
+
+export default new IntegrationDefinition({
+ name: integrationName,
+ title: 'Kommo',
+ version: '0.1.0',
+ readme: 'hub.md',
+ icon: 'icon.svg',
+
+ // User provides these when configuring the integration
+ configuration: {
+ schema: z.object({
+ baseDomain: z
+ .string()
+ .describe('Your Kommo subdomain (e.g., yourcompany.kommo.com)'),
+ accessToken: z
+ .string()
+ .describe('Long-lived access token from your Kommo private integration'),
+ }),
+ },
+ actions,
+})
diff --git a/integrations/kommo/package.json b/integrations/kommo/package.json
new file mode 100644
index 00000000..d36c6668
--- /dev/null
+++ b/integrations/kommo/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "kommo",
+ "integrationName": "kommo",
+ "scripts": {
+ "check:type": "tsc --noEmit"
+ },
+ "private": true,
+ "dependencies": {
+ "@botpress/client": "1.29.0",
+ "@botpress/sdk": "5.1.1",
+ "axios": "^1.13.2"
+ },
+ "devDependencies": {
+ "@types/node": "^22.16.4",
+ "typescript": "^5.6.3"
+ }
+}
diff --git a/integrations/kommo/src/actions/contact.ts b/integrations/kommo/src/actions/contact.ts
new file mode 100644
index 00000000..17995b39
--- /dev/null
+++ b/integrations/kommo/src/actions/contact.ts
@@ -0,0 +1,77 @@
+import * as sdk from '@botpress/sdk'
+import * as bp from '.botpress'
+import { KommoClient, CreateContactRequest, KommoContact, getErrorMessage } from '../kommo-api'
+
+// mapping kommo to local schema
+function mapKommoContactToBotpress(contact: KommoContact) {
+ return {
+ id: contact.id,
+ name: contact.name,
+ firstName: contact.first_name,
+ lastName: contact.last_name,
+ responsibleUserId: contact.responsible_user_id,
+ groupId: contact.group_id,
+ updatedBy: contact.updated_by,
+ createdAt: contact.created_at,
+ updatedAt: contact.updated_at,
+ closestTaskAt: contact.closest_task_at ?? undefined,
+ isDeleted: contact.is_deleted,
+ accountId: contact.account_id,
+ }
+}
+
+export const createContact: bp.IntegrationProps['actions']['createContact'] = async ({
+ ctx,
+ input,
+ logger,
+}) => {
+ try {
+ logger.forBot().info('Creating contact with input:', input)
+
+ const {baseDomain, accessToken} = ctx.configuration
+ const kommoClient = new KommoClient(accessToken, baseDomain, logger)
+
+ const contactData: CreateContactRequest = {
+ name: input.name,
+ first_name: input.firstName,
+ last_name: input.lastName,
+ responsible_user_id: input.responsibleUserId,
+ created_by: input.createdBy,
+ updated_by: input.updatedBy,
+ }
+
+ logger.forBot().info('Contact data to send:', contactData)
+ const kommoContact = await kommoClient.createContact(contactData)
+ logger.forBot().info('Contact created successfully:', { contactId: kommoContact.id })
+ return {
+ contact: mapKommoContactToBotpress(kommoContact),
+ }
+
+ } catch (error) {
+ logger.forBot().error('Failed to create contact', {error})
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+}
+
+export const searchContacts: bp.IntegrationProps['actions']['searchContacts'] = async ({
+ ctx,
+ input,
+ logger,
+}) => {
+ try {
+ logger.forBot().info('Searching contacts:', {query: input.query})
+ const {baseDomain, accessToken} = ctx.configuration
+
+ const kommoClient = new KommoClient(accessToken, baseDomain, logger)
+ const kommoContacts = await kommoClient.searchContacts(input.query)
+
+ const contacts = kommoContacts.map(mapKommoContactToBotpress)
+
+ logger.forBot().info('Search complete:', {count: contacts.length})
+
+ return {contacts}
+ } catch (error) {
+ logger.forBot().error('Failed to search contacts', {error})
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+}
\ No newline at end of file
diff --git a/integrations/kommo/src/actions/index.ts b/integrations/kommo/src/actions/index.ts
new file mode 100644
index 00000000..4dc079a9
--- /dev/null
+++ b/integrations/kommo/src/actions/index.ts
@@ -0,0 +1,2 @@
+export * from './lead'
+export * from './contact'
\ No newline at end of file
diff --git a/integrations/kommo/src/actions/lead.ts b/integrations/kommo/src/actions/lead.ts
new file mode 100644
index 00000000..5053f9d5
--- /dev/null
+++ b/integrations/kommo/src/actions/lead.ts
@@ -0,0 +1,109 @@
+import * as sdk from '@botpress/sdk'
+import * as bp from '.botpress'
+import { KommoClient, CreateLeadRequest, UpdateLeadRequest, KommoLead, getErrorMessage } from '../kommo-api'
+
+// mapping kommo to local schema
+function mapKommoLeadToBotpress(lead: KommoLead) {
+ return {
+ id: lead.id,
+ name: lead.name,
+ price: lead.price,
+ responsibleUserId: lead.responsible_user_id,
+ pipelineId: lead.pipeline_id,
+ statusId: lead.status_id,
+ createdAt: lead.created_at,
+ updatedAt: lead.updated_at,
+ }
+}
+
+export const createLead: bp.IntegrationProps['actions']['createLead'] = async ({
+ ctx,
+ input,
+ logger,
+}) => {
+ try {
+ logger.forBot().info('Creating lead with input:', input)
+ logger.forBot().info('Configuration:', { baseDomain: ctx.configuration.baseDomain })
+
+ const { baseDomain, accessToken } = ctx.configuration
+ const kommoClient = new KommoClient(accessToken, baseDomain, logger)
+
+ const leadData: CreateLeadRequest = {
+ name: input.name,
+ price: input.price,
+ responsible_user_id: input.responsibleUserId,
+ pipeline_id: input.pipelineId,
+ status_id: input.statusId,
+ }
+
+ logger.forBot().info('Lead data to send:', leadData)
+ const kommoLead = await kommoClient.createLead(leadData)
+ logger.forBot().info('Lead created successfully:', { leadId: kommoLead.id })
+
+ return {
+ lead: mapKommoLeadToBotpress(kommoLead),
+ }
+ } catch (error) {
+ logger.forBot().error('Error creating lead:', { error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+}
+
+
+// handler for updating lead
+export const updateLead: bp.IntegrationProps['actions']['updateLead'] = async ({
+ ctx,
+ input,
+ logger,
+}) => {
+ try {
+ logger.forBot().info('Updating lead:', input)
+
+ const { baseDomain, accessToken } = ctx.configuration
+ const kommoClient = new KommoClient(accessToken, baseDomain, logger)
+
+ const updateData: UpdateLeadRequest = {
+ name: input.name,
+ price: input.price,
+ responsible_user_id: input.responsibleUserId,
+ pipeline_id: input.pipelineId,
+ status_id: input.statusId,
+ }
+
+ logger.forBot().info('Update data to send:', updateData)
+ const kommoLead = await kommoClient.updateLead(input.leadId, updateData)
+ logger.forBot().info('Lead updated successfully:', { leadId: kommoLead.id })
+
+ return {
+ lead: mapKommoLeadToBotpress(kommoLead),
+ }
+ } catch (error) {
+ logger.forBot().error('Error updating lead:', { error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+}
+
+// search for lead action
+export const searchLeads: bp.IntegrationProps['actions']['searchLeads'] = async ({
+ ctx,
+ input,
+ logger,
+}) => {
+ try {
+ logger.forBot().info('Searching leads:', { query: input.query })
+
+ const { baseDomain, accessToken } = ctx.configuration
+ const kommoClient = new KommoClient(accessToken, baseDomain, logger)
+ const kommoLeads = await kommoClient.searchLeads(input.query)
+
+ const leads = kommoLeads.map(mapKommoLeadToBotpress)
+
+ logger.forBot().info('Search complete:', { count: leads.length })
+
+ return { leads }
+ } catch (error) {
+ logger.forBot().error('Failed to search leads', { error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+}
+
diff --git a/integrations/kommo/src/index.ts b/integrations/kommo/src/index.ts
new file mode 100644
index 00000000..c14cccab
--- /dev/null
+++ b/integrations/kommo/src/index.ts
@@ -0,0 +1,17 @@
+import * as bp from '.botpress'
+import { createLead, updateLead, createContact, searchContacts, searchLeads} from './actions'
+
+export default new bp.Integration({
+ register: async () => {},
+ unregister: async () => {},
+
+ actions: {
+ createLead,
+ updateLead,
+ searchLeads,
+ createContact,
+ searchContacts,
+ },
+ channels: {},
+ handler: async () => {},
+})
diff --git a/integrations/kommo/src/kommo-api/error-handler.ts b/integrations/kommo/src/kommo-api/error-handler.ts
new file mode 100644
index 00000000..62ea5704
--- /dev/null
+++ b/integrations/kommo/src/kommo-api/error-handler.ts
@@ -0,0 +1,65 @@
+import { isAxiosError } from 'axios'
+import { ZodError, ZodIssue } from '@botpress/sdk'
+import { KommoErrorResponse } from './types'
+
+const formatZodErrors = (issues: ZodIssue[]) =>
+ 'Validation Error: ' +
+ issues
+ .map((issue) => {
+ const path = issue.path?.length ? `${issue.path.join('.')}: ` : ''
+ return path ? `${path}${issue.message}` : issue.message
+ })
+ .join('\n')
+
+export const getErrorMessage = (err: unknown): string => {
+ if (isAxiosError(err)) {
+ // server dependent error
+ const status = err.response?.status
+ const data = err.response?.data
+ // always present
+ const message = err.message
+
+ if (data && typeof data === 'object') {
+ const kommoError = data as KommoErrorResponse
+
+ if (kommoError.detail) {
+ let errorMsg = kommoError.detail
+
+ if (kommoError.validation_errors && kommoError.validation_errors.length > 0) {
+ const validationDetails = kommoError.validation_errors
+ .flatMap((ve) => ve.errors.map((e) => `${e.path}: ${e.detail}`))
+ .join(', ')
+ errorMsg += ` - ${validationDetails}`
+ }
+
+ return status ? `${errorMsg} (Status: ${status})` : errorMsg
+ }
+ }
+
+ // Fallback for generic axios errors
+ if (typeof data === 'string' && data.trim()) {
+ return status ? `${data} (Status: ${status})` : data
+ }
+ return status ? `${message} (Status: ${status})` : message
+ }
+
+ if (err instanceof ZodError) {
+ return formatZodErrors(err.errors)
+ }
+
+ if (err instanceof Error) {
+ return err.message
+ }
+
+ if (typeof err === 'string') {
+ return err
+ }
+
+ if (err && typeof err === 'object' && 'message' in err) {
+ const message = (err as { message: unknown }).message
+ if (typeof message === 'string') {
+ return message
+ }
+ }
+ return 'An unexpected error occurred'
+}
diff --git a/integrations/kommo/src/kommo-api/index.ts b/integrations/kommo/src/kommo-api/index.ts
new file mode 100644
index 00000000..93e96da3
--- /dev/null
+++ b/integrations/kommo/src/kommo-api/index.ts
@@ -0,0 +1,3 @@
+export * from './types'
+export * from './kommo-client'
+export * from './error-handler'
diff --git a/integrations/kommo/src/kommo-api/kommo-client.ts b/integrations/kommo/src/kommo-api/kommo-client.ts
new file mode 100644
index 00000000..77377af6
--- /dev/null
+++ b/integrations/kommo/src/kommo-api/kommo-client.ts
@@ -0,0 +1,189 @@
+import axios, { AxiosInstance, AxiosError } from 'axios'
+import * as sdk from '@botpress/sdk'
+import * as bp from '../../.botpress'
+import { CreateLeadRequest, KommoLead, KommoCreateResponse, UpdateLeadRequest } from './types'
+import { CreateContactRequest, KommoContact, KommoCreateContactResponse, KommoSearchContactsResponse, KommoSearchLeadResponse } from './types'
+import { getErrorMessage } from './error-handler'
+
+
+export class KommoClient {
+ private _axios: AxiosInstance
+ private _logger: bp.Logger
+
+ constructor(accessToken: string, baseDomain: string, logger: bp.Logger) {
+ this._logger = logger
+
+ // Ensure basedomain doesn't have protocol prefix or trailing slash
+ const cleanDomain = baseDomain.replace(/^https?:\/\//, '').replace(/\/$/, '')
+
+ // Create axios instance with Kommo API configuration
+ this._axios = axios.create({
+ baseURL: `https://${cleanDomain}/api/v4`,
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ })
+
+ this._logger.forBot().debug('KommoClient initialized', {
+ baseURL: `https://${cleanDomain}/api/v4`
+ })
+ }
+
+// -----LEADS------
+ async createLead(data: CreateLeadRequest): Promise {
+ try {
+ this._logger.forBot().debug('Creating lead in Kommo', { name: data.name })
+ const response = await this._axios.post('/leads', [data])
+ const createdLeadId = response.data._embedded.leads[0]?.id
+
+ if (!createdLeadId) {
+ throw new sdk.RuntimeError('No lead ID returned from Kommo')
+ }
+
+ const lead = await this.getLead(createdLeadId)
+
+ if (!lead) {
+ throw new sdk.RuntimeError('Failed to fetch created lead')
+ }
+
+ this._logger.forBot().info('Lead created successfully', { leadId: lead.id })
+ return lead
+ } catch (error) {
+ this._logger.forBot().error('Failed to create lead', { error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+
+
+ // gets a lead by id
+ async getLead(leadId: number): Promise {
+ try {
+ this._logger.forBot().debug('Fetching lead from Kommo', { leadId })
+
+ const response = await this._axios.get(`/leads/${leadId}`)
+ const lead = response.data
+
+ return lead
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ this._logger.forBot().info('Lead not found', { leadId })
+ return undefined
+ }
+
+ this._logger.forBot().error('Failed to fetch lead', { leadId, error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+
+ // update a single lead
+ async updateLead(leadId: number, data: UpdateLeadRequest): Promise {
+ try {
+ this._logger.forBot().debug('Updating lead in Kommo', { leadId, data })
+ await this._axios.patch(`/leads/${leadId}`, data)
+ const lead = await this.getLead(leadId)
+
+ if (!lead) {
+ throw new sdk.RuntimeError('Failed to fetch updated lead')
+ }
+
+ this._logger.forBot().info('Lead updated successfully', { leadId: lead.id })
+ return lead
+ } catch (error) {
+ this._logger.forBot().error('Failed to update lead', { leadId, error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+
+ // search leads by query
+ async searchLeads(query: string): Promise {
+ try {
+ this._logger.forBot().debug('Searching for leads', { query })
+ const response = await this._axios.get('/leads', {
+ params: { query }
+ })
+
+ const leads = response.data._embedded?.leads || []
+
+ this._logger.forBot().info('Leads found:', { count: leads.length })
+ return leads
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ this._logger.forBot().info('No leads found', { query })
+ return []
+ }
+ this._logger.forBot().error('Failed to search leads', { query, error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+// -----Contacts-----
+ async createContact(data: CreateContactRequest): Promise {
+ try {
+ this._logger.forBot().debug('Creating contact in Kommo', { name: data.name })
+ // contacts sent to kommo as an array
+ const response = await this._axios.post('/contacts', [data])
+
+ // get the ID from the response
+ const createdContactId = response.data._embedded.contacts[0]?.id
+ if (!createdContactId) {
+ throw new sdk.RuntimeError('No contact ID returned from Kommo')
+ }
+ // fetch full contact details to return to user
+ const contact = await this.getContact(createdContactId)
+ if (!contact) {
+ throw new sdk.RuntimeError('Failed to fetch created contact')
+ }
+ this._logger.forBot().info('Contact created successfully', { contactId: contact.id })
+
+ return contact
+ } catch (error) {
+ this._logger.forBot().error('Failed to create contact', { error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+
+ // internal method for create contact
+ async getContact(contactId: number): Promise {
+ try {
+ this._logger.forBot().debug('Fetching contact from Kommo', { contactId })
+
+ const response = await this._axios.get(`/contacts/${contactId}`)
+ const contact = response.data
+ return contact
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ this._logger.forBot().info('Contact not found', { contactId })
+ return undefined
+ }
+
+ this._logger.forBot().error('Failed to fetch contact', { contactId, error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+
+ // method to search contacts by phone number, name or email
+ async searchContacts(query: string): Promise {
+ try {
+ this._logger.forBot().debug('Searching contacts in Kommo', { query })
+
+ const response = await this._axios.get('/contacts', {
+ params: { query }
+ })
+
+ const contacts = response.data._embedded?.contacts || []
+
+ this._logger.forBot().info('Contacts found:', { count: contacts.length })
+ return contacts
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ this._logger.forBot().info('No contacts found', { query })
+ return []
+ }
+
+ this._logger.forBot().error('Failed to search contacts', { query, error })
+ throw new sdk.RuntimeError(getErrorMessage(error))
+ }
+ }
+}
+
+
diff --git a/integrations/kommo/src/kommo-api/types.ts b/integrations/kommo/src/kommo-api/types.ts
new file mode 100644
index 00000000..666713f8
--- /dev/null
+++ b/integrations/kommo/src/kommo-api/types.ts
@@ -0,0 +1,245 @@
+
+// -----LEADS------
+
+export type KommoLead = {
+ id: number
+ name: string
+ price: number
+ responsible_user_id: number
+ group_id: number
+ status_id: number
+ pipeline_id: number
+ loss_reason_id: number | null
+ created_by: number
+ updated_by: number
+ created_at: number
+ updated_at: number
+ closed_at: number | null
+ closest_task_at: number | null
+ is_deleted: boolean
+ score: number | null
+ account_id: number
+ labor_cost: number | null
+ is_price_computed: boolean
+
+ custom_fields_values?: Array<{
+ field_id: number
+ field_name: string
+ field_code?: string | null
+ field_type: string
+ values: Array<{
+ value: string | number
+ enum_id?: number
+ }>
+ }>
+
+ // Embedded relationships (contacts, companies, tags)
+ _embedded?: {
+ tags?: Array<{
+ id: number
+ name: string
+ }>
+ companies?: Array<{
+ id: number
+ _links: {
+ self: {
+ href: string
+ }
+ }
+ }>
+ contacts?: Array<{
+ id: number
+ is_main: boolean
+ _links: {
+ self: {
+ href: string
+ }
+ }
+ }>
+ }
+ _links?: {
+ self: {
+ href: string
+ }
+ }
+}
+
+
+export type CreateLeadRequest = {
+ name: string // REQUIRED - only required field!
+ price?: number
+ responsible_user_id?: number
+ pipeline_id?: number
+ status_id?: number
+ created_by?: number
+ updated_by?: number
+ created_at?: number
+ updated_at?: number
+ closed_at?: number
+
+ // Custom fields
+ custom_fields_values?: Array<{
+ field_id: number
+ values: Array<{
+ value: string | number
+ }>
+ }>
+
+ // Embedded data (tags, contacts, companies)
+ _embedded?: {
+ tags?: Array<{
+ id?: number
+ name?: string
+ }>
+ contacts?: Array<{
+ id: number
+ is_main?: boolean
+ }>
+ companies?: Array<{
+ id: number
+ }>
+ }
+}
+
+
+export type UpdateLeadRequest = {
+ id?: number
+ name?: string
+ price?: number
+ responsible_user_id?: number
+ status_id?: number
+ pipeline_id?: number
+ custom_fields_values?: Array<{
+ field_id: number
+ values: Array<{
+ value: string | number
+ }>
+ }>
+}
+
+
+export type KommoCreateResponse = {
+ _links: {
+ self: {
+ href: string
+ }
+ }
+ _embedded: {
+ leads: Array<{
+ id: number
+ request_id: string
+ _links: {
+ self: {
+ href: string
+ }
+ }
+ }>
+ }
+}
+
+
+export type KommoUpdateResponse = {
+ _links: {
+ self: {
+ href: string
+ }
+ }
+ _embedded: {
+ leads: KommoLead[]
+ }
+}
+
+export type KommoSearchLeadResponse = {
+ _page: number
+ _links: {
+ self: {
+ href: string
+ }
+ }
+ _embedded: {
+ leads: KommoLead[]
+ }
+}
+
+
+// -------------CONTACTS-------------
+
+// full contact with all details
+export type KommoContact = {
+ id: number;
+ name: string;
+ first_name: string;
+ last_name: string;
+ responsible_user_id: number;
+ group_id: number;
+ updated_by: number;
+ created_at: number;
+ updated_at: number;
+ closest_task_at: number | null;
+ is_deleted: boolean;
+ account_id: number;
+
+};
+
+
+// sends data to kommo
+export type CreateContactRequest = {
+ name?: string;
+ first_name?: string
+ last_name?: string;
+ responsible_user_id: number;
+ created_by: number;
+ updated_by?: number;
+};
+
+// what kommo returns after creating contats
+export type KommoCreateContactResponse = {
+ _links:{
+ self:{
+ href:string;
+ };
+ };
+ _embedded:{
+ contacts: Array<{
+ id:number;
+ request_id: string;
+ _links:{
+ self:{
+ href:string;
+ };
+ };
+ }>;
+ };
+}
+
+// for searching contacts by phone number
+export type KommoSearchContactsResponse = {
+ _page: number;
+ _links: {
+ self: {
+ href: string;
+ };
+ next?: {
+ href: string;
+ };
+ };
+ _embedded: {
+ contacts: KommoContact[];
+ };
+}
+
+//----General-----
+export type KommoErrorResponse = {
+ title: string
+ type: string
+ status: number
+ detail: string
+ validation_errors?: Array<{
+ request_id: string
+ errors: Array<{
+ code: string
+ path: string
+ detail: string
+ }>
+ }>
+}
\ No newline at end of file
diff --git a/integrations/kommo/tsconfig.json b/integrations/kommo/tsconfig.json
new file mode 100644
index 00000000..462fc49c
--- /dev/null
+++ b/integrations/kommo/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "lib": ["es2022"],
+ "module": "commonjs",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedParameters": true,
+ "target": "es2017",
+ "baseUrl": ".",
+ "outDir": "dist",
+ "checkJs": false,
+ "exactOptionalPropertyTypes": false,
+ "resolveJsonModule": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "noUnusedLocals": false
+ },
+ "include": [".botpress/**/*", "src/**/*", "*.ts", "*.json"]
+}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 779fd1d9..2375e1a0 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -3,3 +3,4 @@ packages:
- 'integrations/apify'
- 'integrations/zoom'
- 'integrations/hubspot-help-desk-hitl'
+ - 'integrations/kommo'