AI Agent Integration Guide

This guide explains how AI agents and LLM-powered applications can integrate with the InCRM API for automated CRM operations, data retrieval, and workflow management.

Quick Start for AI Agents

  1. Get an API key — create one via the UI or API (see Authentication)
  2. Fetch the OpenAPI specGET /api/v1/openapi.json (no auth required)
  3. Start making requests — use the API key in the X-Api-Key header

OpenAPI Specification

The full API schema is available at:

GET /api/v1/openapi.json

This endpoint requires no authentication and returns an OpenAPI 3.0 document. Use it to:

Fetching the Spec

curl https://api.incrm.app/api/v1/openapi.json -o openapi.json
const response = await fetch('https://api.incrm.app/api/v1/openapi.json')
const spec = await response.json()

Using the TypeScript SDK

The @incrm-app/sdk package provides fully typed functions for every API endpoint, generated from the OpenAPI spec.

Setup

import { InCRMClient, listClients, getClient, createInvoice } from '@incrm-app/sdk'

const incrm = new InCRMClient({
  baseUrl: 'https://api.incrm.app',
  apiKey: process.env.INCRM_API_KEY!,
})

Common Operations

List all clients:

const { data: response } = await listClients({ query: { page: 1, perPage: 100 } })
const clients = response.data
const totalClients = response.meta.total

Get a specific client:

const { data: response } = await getClient({ path: { id: 'client-id' } })
const client = response.data

Create an invoice:

const { data: response } = await createInvoice({
  body: {
    clientId: 'client-id',
    items: [
      { description: 'Consulting', quantity: 2, unitPrice: 150 },
    ],
  },
})

LLM Function Calling

OpenAI Function Calling

Convert OpenAPI endpoints to OpenAI function definitions:

import { readFileSync } from 'fs'

const spec = JSON.parse(readFileSync('openapi.json', 'utf-8'))

function endpointToFunction(path: string, method: string, operation: any) {
  return {
    type: 'function',
    function: {
      name: operation.operationId,
      description: operation.summary || `${method.toUpperCase()} ${path}`,
      parameters: extractParameters(operation),
    },
  }
}

LangChain Tools

Build LangChain-compatible tools from the API:

import { DynamicStructuredTool } from '@langchain/core/tools'
import { z } from 'zod'
import { InCRMClient, listClients, createClient } from '@incrm-app/sdk'

const incrm = new InCRMClient({
  baseUrl: 'https://api.incrm.app',
  apiKey: process.env.INCRM_API_KEY!,
})

const listClientsTool = new DynamicStructuredTool({
  name: 'list_clients',
  description: 'List CRM clients with optional filtering and pagination',
  schema: z.object({
    page: z.number().optional().describe('Page number (default: 1)'),
    perPage: z.number().optional().describe('Items per page (default: 10, max: 100)'),
    where: z.string().optional().describe('Filter expression, e.g. "firstName:John"'),
  }),
  func: async ({ page, perPage, where }) => {
    const { data } = await listClients({ query: { page, perPage, where } })
    return JSON.stringify(data)
  },
})

const createClientTool = new DynamicStructuredTool({
  name: 'create_client',
  description: 'Create a new CRM client record',
  schema: z.object({
    firstName: z.string().describe('Client first name'),
    lastName: z.string().describe('Client last name'),
    email: z.string().optional().describe('Client email address'),
    phone: z.string().optional().describe('Client phone number'),
  }),
  func: async (params) => {
    const { data } = await createClient({ body: params })
    return JSON.stringify(data)
  },
})

Pagination Traversal Pattern

When an AI agent needs to process all records of a certain type, use this traversal pattern:

async function fetchAllRecords<T>(
  fetcher: (query: { page: number; perPage: number }) => Promise<{
    data: { data: T[]; meta: { next: number | null } }
  }>,
): Promise<T[]> {
  const allRecords: T[] = []
  let page = 1

  while (true) {
    const { data: response } = await fetcher({ page, perPage: 100 })
    allRecords.push(...response.data)

    if (!response.meta.next) break
    page = response.meta.next

    // Respect rate limits
    await new Promise(resolve => setTimeout(resolve, 200))
  }

  return allRecords
}

// Usage
const allClients = await fetchAllRecords((query) => listClients({ query }))
const allInvoices = await fetchAllRecords((query) => listInvoices({ query }))

Rate Limit Awareness

AI agents must respect rate limits to avoid service disruption. The global limit is 50 requests per 60 seconds.

Strategies

  1. Monitor headers — check X-RateLimit-Remaining after each request.
  2. Add delays — insert 200ms+ delays between sequential requests.
  3. Batch with pagination — use perPage=100 to minimize request count.
  4. Handle 429 gracefully — implement exponential backoff on rate limit errors.
async function safeRequest<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn()
  } catch (error: any) {
    if (error.status === 429) {
      const resetHeader = error.headers?.['x-ratelimit-reset']
      const waitMs = resetHeader
        ? Math.max(0, Number(resetHeader) * 1000 - Date.now())
        : 5000

      await new Promise(resolve => setTimeout(resolve, waitMs))
      return fn()
    }
    throw error
  }
}

Permission Model

API keys can be scoped to specific permissions, limiting what an AI agent can do.

Recommended Scopes for AI Agents

Read-only agent (data analysis, reporting):

{
  "name": "AI Read Agent",
  "scopes": [
    "clients.read",
    "invoices.read",
    "quotes.read",
    "products.read",
    "services.read",
    "employees.read"
  ]
}

CRM automation agent (create and update records):

{
  "name": "AI CRM Agent",
  "scopes": [
    "clients.read", "clients.create", "clients.update",
    "invoices.read", "invoices.create",
    "quotes.read", "quotes.create", "quotes.update"
  ]
}

Full access (omit scopes or leave empty):

{
  "name": "AI Full Access",
  "scopes": []
}

An empty scopes array inherits all permissions from the key owner.

Handling Permission Errors

When an agent attempts an action outside its scope, the API returns:

{
  "success": false,
  "error": {
    "code": 401014,
    "message": "Insufficient permissions for this action"
  }
}

The agent should report this to the user rather than retrying.

Multi-Tenant Context

Each API key is bound to a specific business. All data returned by the API is scoped to that business — the agent cannot access data from other tenants.

This means:

Webhook-Driven Agents

Instead of polling the API, AI agents can react to events in real-time using webhooks:

  1. Register a webhook for events your agent cares about (e.g., client.created, invoice.updated)
  2. Receive events at your webhook URL as they happen
  3. Process and act — use the event data to trigger agent workflows

This is more efficient than polling and ensures your agent responds to changes within seconds.

See Webhooks for setup instructions and signature verification.

Example: Complete AI Agent Setup

import { InCRMClient, listClients, createInvoice } from '@incrm-app/sdk'

// 1. Initialize the client
const incrm = new InCRMClient({
  baseUrl: 'https://api.incrm.app',
  apiKey: process.env.INCRM_API_KEY!,
})

// 2. Fetch all clients
const { data: clientsResponse } = await listClients({
  query: { page: 1, perPage: 100, orderBy: 'createdAt:desc' },
})

console.log(`Found ${clientsResponse.meta.total} clients`)

// 3. Process each client
for (const client of clientsResponse.data) {
  console.log(`${client.firstName} ${client.lastName} (${client.email})`)
}

// 4. Create an invoice for the first client
if (clientsResponse.data.length > 0) {
  const { data: invoiceResponse } = await createInvoice({
    body: {
      clientId: clientsResponse.data[0].id,
      items: [
        { description: 'Monthly retainer', quantity: 1, unitPrice: 2000 },
      ],
    },
  })

  console.log(`Created invoice: ${invoiceResponse.data.id}`)
}