# How to Integrate Lovable with Zoho CRM

- Tool: Lovable
- Difficulty: Intermediate
- Time required: 35 minutes
- Last updated: March 2026

## TL;DR

To integrate Zoho CRM with Lovable, register an OAuth2 Server-Based Application in the Zoho Developer Console, complete the authorization flow to obtain access and refresh tokens, store all credentials in Cloud → Secrets, then build Edge Functions that call the Zoho CRM API v2 for records, modules, and workflows. Zoho CRM is the right choice when you are already in the Zoho ecosystem with Zoho Books, Zoho Desk, or other Zoho apps.

## Connect your Lovable app to Zoho CRM's full record and workflow API

Zoho CRM is the sales hub of the Zoho ecosystem — a platform most valuable to teams already using Zoho Books for accounting, Zoho Desk for support, Zoho Campaigns for email marketing, or any of the 40+ Zoho applications. When your CRM data flows bidirectionally between your Lovable app and Zoho CRM, your sales and operations teams can work in Zoho while your customers interact through the custom-branded Lovable interface.

Zoho CRM API v2 covers the complete CRM data model: Leads, Contacts, Accounts, Deals (Potentials), Activities, Campaigns, and Custom Modules. The API supports CRUD operations on all standard and custom modules, bulk operations for importing large datasets, and triggering CRM workflows via API-initiated record changes. All requests authenticate via OAuth2 Bearer tokens, and Zoho's well-documented API supports both field-level filtering and full-text search.

The most important technical detail for Zoho CRM integration is region-specific authentication. Zoho operates data centers in the US (accounts.zoho.com), EU (accounts.zoho.eu), India (accounts.zoho.in), Australia (accounts.zoho.com.au), and Japan (accounts.zoho.jp). Both the OAuth2 authorization endpoints and the CRM API base URLs differ by region. Storing the base URL as a secret ensures your Edge Function works regardless of which data center your Zoho account is hosted on.

For Lovable apps, the most common use cases are bidirectional contact sync (new app users become CRM Contacts or Leads), deal creation when users upgrade or request demos, and custom dashboards that surface CRM data to sales and operations teams without requiring full Zoho CRM access for every team member.

## Before you start

- A Lovable project with Lovable Cloud enabled (Edge Functions require a deployed app)
- A Zoho CRM account (free plan available for up to 3 users at zoho.com/crm)
- Access to the Zoho Developer Console (api-console.zoho.com) to create the OAuth2 application
- Your Zoho account's data center region (US, EU, India, Australia, or Japan — check your login URL)
- A tool to make HTTP POST requests for the initial OAuth2 token exchange (Postman, Insomnia, or Hoppscotch)

## Step-by-step guide

### 1. Register an OAuth2 Server-Based Application in Zoho Developer Console

Zoho CRM uses OAuth2 for all API authentication. You must register your integration as a 'Server-Based Application' in the Zoho Developer Console before making any API calls.

Go to api-console.zoho.com and sign in with your Zoho account. Click 'Add Client'. Choose 'Server-based Applications' — this is the correct type for Edge Function server-to-server calls. Fill in:
- Client Name: 'Lovable CRM Integration' (or any descriptive name)
- Homepage URL: your Lovable app URL
- Authorized Redirect URIs: https://yourapp.lovable.app/oauth/callback (used for the one-time code exchange)

After saving, Zoho displays a Client ID and Client Secret. Copy both.

Now, generate the authorization code. Construct this URL and open it in your browser (replace YOUR_CLIENT_ID and YOUR_REDIRECT_URI):
https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.modules.ALL,ZohoCRM.settings.ALL&client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&access_type=offline

For EU accounts, use accounts.zoho.eu; for India, accounts.zoho.in. The 'access_type=offline' parameter is required for a refresh token. After you authorize, Zoho redirects to your redirect URI with a 'code' parameter. Copy the code value — it expires in 10 minutes.

Exchange the code for tokens: POST to https://accounts.zoho.com/oauth/v2/token with grant_type=authorization_code, client_id, client_secret, redirect_uri, and code. Use Postman or similar — send as form URL-encoded body. The response includes access_token, refresh_token, expires_in, and api_domain. Copy all four values.

**Expected result:** You have a Zoho Client ID, Client Secret, Access Token, Refresh Token, and API Domain. The tokens are ready to be stored in Lovable Cloud → Secrets.

### 2. Store all OAuth2 credentials in Cloud Secrets

Zoho CRM OAuth2 integration requires five secrets in Lovable's Cloud Secrets panel. In Lovable, click '+' → Cloud panel → Secrets tab. Add:

- ZOHO_CRM_CLIENT_ID — your application Client ID from the Zoho Developer Console
- ZOHO_CRM_CLIENT_SECRET — your application Client Secret
- ZOHO_CRM_ACCESS_TOKEN — the current access token (valid for one hour from generation)
- ZOHO_CRM_REFRESH_TOKEN — the long-lived refresh token (used to get new access tokens)
- ZOHO_CRM_BASE_URL — the API base URL from the token exchange api_domain field (e.g., https://www.zohoapis.com)

The accounts domain (used for token refresh) is derived from the base URL by replacing 'www.zohoapis' with 'accounts.zoho'. For example, if ZOHO_CRM_BASE_URL is https://www.zohoapis.eu, the token refresh URL is https://accounts.zoho.eu/oauth/v2/token.

Zoho CRM access tokens expire after one hour. Your Edge Function implements automatic refresh: when a 401 is returned, use the client credentials and refresh token to obtain a new access token. The refresh token itself does not expire as long as it is used at least once every 60 days.

For Zoho CRM specifically, the default module names used in API paths are: Leads, Contacts, Accounts, Deals (not Opportunities), Activities, Calls, Meetings. Custom modules use their API name (visible in Zoho CRM → Settings → Modules and Fields).

**Expected result:** Five secrets appear in Cloud → Secrets: ZOHO_CRM_CLIENT_ID, ZOHO_CRM_CLIENT_SECRET, ZOHO_CRM_ACCESS_TOKEN, ZOHO_CRM_REFRESH_TOKEN, and ZOHO_CRM_BASE_URL.

### 3. Create the Zoho CRM API proxy Edge Function with token refresh

Build the Edge Function that handles Zoho CRM operations with automatic OAuth2 token refresh. The function uses the same pattern as the Zoho Books Edge Function: attempt the API call, and if a 401 is returned, refresh the token using the client credentials and retry.

Zoho CRM API v2 endpoints follow the pattern: GET/POST/PUT/DELETE https://www.zohoapis.com/crm/v2/{module}. Records are wrapped in a 'data' array in both requests and responses. Create operations POST an array with one or more record objects; the response includes per-record success/error status in the 'data' array.

For searching, Zoho CRM provides a dedicated search endpoint: GET /crm/v2/{module}/search?criteria=(fieldName:equals:value). Multiple criteria use 'and' or 'or' operators: (Email:equals:user@example.com) and (Company:equals:Acme). This is far more reliable than the full-text search endpoint for field-specific lookups.

For fetching records, use GET /crm/v2/{module} for all records (paginated by default at 200 per page), or GET /crm/v2/{module}/{recordId} for a specific record. The response includes 'info' with pagination data.

Zoho CRM API v2 has a rate limit of 5,000 API calls per day for free accounts and 25,000 for paid plans. For production integrations with high-volume syncing, implement request batching using Zoho's bulk CRUD endpoints.

```
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

async function refreshToken(): Promise<string> {
  const baseUrl = Deno.env.get('ZOHO_CRM_BASE_URL')!
  const accountsDomain = baseUrl.replace('www.zohoapis', 'accounts.zoho')
  const resp = await fetch(`${accountsDomain}/oauth/v2/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: Deno.env.get('ZOHO_CRM_CLIENT_ID')!,
      client_secret: Deno.env.get('ZOHO_CRM_CLIENT_SECRET')!,
      refresh_token: Deno.env.get('ZOHO_CRM_REFRESH_TOKEN')!,
    }),
  })
  const data = await resp.json()
  if (!data.access_token) throw new Error('Token refresh failed')
  return data.access_token
}

async function crmFetch(path: string, method = 'GET', body?: unknown, retry = true): Promise<unknown> {
  const base = Deno.env.get('ZOHO_CRM_BASE_URL')!
  let token = Deno.env.get('ZOHO_CRM_ACCESS_TOKEN')!

  let resp = await fetch(`${base}/crm/v2${path}`, {
    method,
    headers: {
      Authorization: `Zoho-oauthtoken ${token}`,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  })

  if (resp.status === 401 && retry) {
    token = await refreshToken()
    resp = await fetch(`${base}/crm/v2${path}`, {
      method,
      headers: {
        Authorization: `Zoho-oauthtoken ${token}`,
        'Content-Type': 'application/json',
      },
      body: body ? JSON.stringify(body) : undefined,
    })
  }
  return resp.json()
}

serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  try {
    const { action, module: mod = 'Leads', params = {} } = await req.json()
    let result

    switch (action) {
      case 'create_record':
        result = await crmFetch(`/${mod}`, 'POST', { data: [params] })
        break
      case 'search_records':
        result = await crmFetch(`/${mod}/search?criteria=${encodeURIComponent(params.criteria)}&per_page=${params.perPage || 20}`)
        break
      case 'get_record':
        result = await crmFetch(`/${mod}/${params.recordId}`)
        break
      case 'update_record':
        result = await crmFetch(`/${mod}/${params.recordId}`, 'PUT', { data: [params.fields] })
        break
      default:
        return new Response(JSON.stringify({ error: 'Unknown action' }), {
          status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        })
    }

    return new Response(JSON.stringify(result), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    })
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    })
  }
})
```

**Expected result:** The zoho-crm Edge Function deploys in Cloud → Edge Functions. Test with action 'search_records', module 'Contacts', and a search criteria — Zoho CRM contact records should appear in the response.

### 4. Build CRM-powered features in your Lovable app

With the Edge Function deployed, wire it into your Lovable app's key user flows. Call supabase.functions.invoke('zoho-crm') with the appropriate action, module, and params wherever CRM data needs to flow.

For lead creation from signup flows: invoke after Supabase Auth creates the user account, passing name, email, company, and a Lead_Source value matching your Zoho CRM picklist. Use the fire-and-forget pattern (no await) so CRM creation does not slow down the registration success response.

For CRM dashboards visible to your team, fetch Deals or Contacts with the search_records action using criteria to filter by relevant fields. Zoho CRM search criteria use the format: (Field_Name:operator:value). Operators are 'equals', 'not_equal', 'contains', 'starts_with', 'greater_equal', 'lesser_equal'. Combine with 'and' or 'or': ((Stage:equals:Qualification)and(Amount:greater_equal:10000)).

For account-level operations that involve multiple modules (creating an Account, Contact, and Deal together), make the API calls sequentially in the Edge Function: create Account → create Contact with Account_Name → create Deal with Account_Name and Contact_Name. Zoho CRM links records by name fields in most cases, though record IDs are more reliable for associations.

For complex multi-module integration workflows such as triggering Zoho CRM workflow rules via API-initiated record changes or using Zoho's Blueprint (process management) API, RapidDev's team can help design the correct Edge Function architecture.

**Expected result:** The CRM dashboard displays Zoho CRM deals in a table. The Create Lead form successfully creates records in Zoho CRM that appear in Zoho CRM → Leads. The dashboard loads correctly after the initial OAuth2 token expires, confirming token refresh is working.

## Best practices

- Store the api_domain from the token exchange response as ZOHO_CRM_BASE_URL — it is the authoritative base URL for your account's data center and must match exactly.
- Use Zoho CRM field API names (Last_Name, Lead_Source) not display labels when constructing API payloads — Zoho silently drops fields with incorrect names.
- Implement automatic OAuth2 token refresh in your Edge Function — Zoho access tokens expire after one hour and manual renewal is not viable in production.
- Request only the OAuth2 scopes you need — 'ZohoCRM.modules.ALL' provides full access; use 'ZohoCRM.modules.Leads.CREATE' for narrower write-only integration.
- Use Zoho CRM's /search endpoint with criteria parameters for field-specific lookups rather than fetching all records and filtering in code.
- Implement bulk operations (POST /Leads with up to 100 records) for high-volume syncing rather than making one API call per record.
- Respect Zoho's API rate limits — free accounts get 5,000 calls/day; implement request queuing or batching for features that trigger frequent syncs.

## Use cases

### Create a Zoho CRM Lead when a user signs up or requests a demo

When a prospective customer fills out a demo request form or signs up for a trial in your Lovable app, an Edge Function creates a Zoho CRM Lead record with their details, source attribution, and custom fields. Zoho CRM's lead qualification workflow can then automatically assign the lead to the right salesperson based on territory, score it with Zia AI, and trigger an email sequence — all without any manual data entry.

Prompt example:

```
Create an Edge Function called zoho-crm-lead that accepts { firstName, lastName, email, company, phone, leadSource, description } and creates a Lead record in Zoho CRM using ZOHO_CRM_ACCESS_TOKEN and ZOHO_CRM_BASE_URL from secrets. Map leadSource to Zoho's Lead_Source field. Return the created lead ID. Call this Edge Function from my demo request form after saving to Supabase.
```

### Build a CRM dashboard showing Zoho Deals by stage

Fetch Deals from Zoho CRM filtered by pipeline stage and display them in a custom dashboard inside your Lovable app. Sales managers see their entire pipeline, click into individual deals for details, and update deal amounts or close dates — changes sync back to Zoho CRM in real time. This gives sales leaders a custom view tailored to their workflow without restructuring Zoho CRM itself.

Prompt example:

```
Create Edge Functions to fetch Zoho CRM Deals filtered by stage (Qualification, Proposal, Negotiation, Closed Won). Build a CRM dashboard with pipeline stage columns showing deal name, account name, amount, and close date. Add click-through to a deal detail panel. Include a stage update button that calls a PATCH endpoint to update the Stage field in Zoho CRM. Handle empty states for each column.
```

### Sync Lovable app account data to Zoho CRM Accounts module

When a paying customer upgrades their plan or updates their company profile in your Lovable app, sync the changes to their corresponding Zoho CRM Account record. The Edge Function uses the Zoho CRM search API to find the account by email domain or name, then updates the relevant fields. This keeps your CRM account data current without requiring operations to manually update records in Zoho.

Prompt example:

```
Create an Edge Function called zoho-sync-account that accepts { companyName, domain, plan, mrr, industry, employeeCount } and searches for a matching Zoho CRM Account by domain field, then updates the account's fields using ZOHO_CRM_ACCESS_TOKEN and ZOHO_CRM_BASE_URL from secrets. If no account is found, create a new one. Return the account ID and whether it was created or updated.
```

## Troubleshooting

### Zoho CRM API returns 'INVALID_TOKEN' or 401 after the first hour

Cause: Zoho CRM OAuth2 access tokens expire after one hour. The token stored in Cloud → Secrets is no longer valid.

Solution: Implement the token refresh logic shown in the Edge Function code above. When a 401 or INVALID_TOKEN response is received, call the token refresh endpoint with your client credentials and refresh token to obtain a new access token. For development, manually regenerate the access token via the Zoho Developer Console and update the ZOHO_CRM_ACCESS_TOKEN secret.

### Record creation returns success (201) but required fields are missing in Zoho CRM

Cause: Field names in the request payload use display labels (e.g., 'Last Name') instead of API names (e.g., 'Last_Name'). Zoho silently ignores fields with incorrect API names.

Solution: In Zoho CRM, go to Settings → Modules and Fields → select the module → view the field list. Each field shows its API name in the settings. Use these exact API names in your Edge Function payload. Common examples: First_Name, Last_Name, Email, Phone, Account_Name, Lead_Source, Industry.

### Search criteria returns 'INVALID_QUERY' error

Cause: The Zoho CRM search criteria syntax is incorrect. Common mistakes include wrong field API names, missing parentheses, or using unsupported operators.

Solution: Zoho CRM search criteria must use field API names with exact syntax: (Field_Name:operator:value). For multiple conditions: ((Field1:equals:val1)and(Field2:contains:val2)). Test your criteria string in Zoho CRM's API documentation explorer before using in code. URL-encode the criteria string when passing as a query parameter.

```
// Correct search criteria format:
const criteria = encodeURIComponent(`(Email:equals:${email})`)
const url = `${base}/crm/v2/Contacts/search?criteria=${criteria}`
```

### Zoho API calls fail with 'AUTHENTICATION_FAILURE' even with correct credentials

Cause: The API base URL is incorrect for your Zoho account's data center region.

Solution: Verify the ZOHO_CRM_BASE_URL secret matches your account's region. Check the api_domain field from the original token exchange response — this is the authoritative base URL for your account. Update the secret if the URL does not match your account's data center (US: www.zohoapis.com, EU: www.zohoapis.eu, India: www.zohoapis.in).

## Frequently asked questions

### What is the difference between Zoho CRM and Zoho One?

Zoho CRM is a standalone sales CRM product. Zoho One is a bundle of 40+ Zoho applications (including CRM, Books, Desk, Campaigns, Projects, and more) at $37/user/month. If you are integrating with multiple Zoho products from Lovable, Zoho One can be more cost-effective than paying for individual apps. The API structure and OAuth2 authentication are the same regardless of whether you have Zoho CRM standalone or as part of Zoho One.

### Does Zoho CRM have a free plan that supports API access?

Yes — Zoho CRM has a free plan supporting up to 3 users with full API access (limited to 5,000 API calls per day). This is sufficient for building and testing a Lovable integration. The Standard plan ($14/user/month) increases limits and adds email marketing, workflow automation, and more detailed analytics. For most Lovable integrations, the free tier is an excellent starting point.

### How do I handle Zoho CRM's different data center regions?

Your Zoho account is hosted in a specific data center based on where it was created. The data center determines both the OAuth2 authorization URL (accounts.zoho.com vs accounts.zoho.eu) and the CRM API base URL (www.zohoapis.com vs www.zohoapis.eu). The easiest way to identify your region is the api_domain field returned in the initial token exchange response. Store this URL as ZOHO_CRM_BASE_URL and derive the accounts domain by replacing 'www.zohoapis' with 'accounts.zoho'.

### Can I use the same OAuth2 application for both Zoho CRM and Zoho Books?

Yes — one Zoho OAuth2 application can be authorized with scopes covering multiple Zoho products. Include scopes for both products in the authorization URL: scope=ZohoCRM.modules.ALL,ZohoBooks.fullaccess.all. You will receive a single set of access and refresh tokens that work for both APIs. This simplifies credential management when building multi-product Zoho integrations in Lovable.

### Why do my Zoho CRM records show blank fields even though the API call returns 200?

This is almost always caused by using incorrect field API names. Zoho CRM silently ignores fields whose names do not match the exact API name defined in the module settings. The display label shown in the Zoho CRM UI (e.g., 'Last Name') is different from the API name (e.g., 'Last_Name'). Find the correct API names in Zoho CRM → Settings → Modules and Fields → select module → view each field's settings panel.

---

Source: https://www.rapidevelopers.com/lovable-integration/zoho-crm
© RapidDev — https://www.rapidevelopers.com/lovable-integration/zoho-crm
