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.
Integration method
Zoho CRM has no native Lovable connector. All CRM operations are proxied through Lovable Edge Functions that call the Zoho CRM API v2. OAuth2 access and refresh tokens are stored in Cloud → Secrets, and the Edge Function implements automatic token refresh using Deno.env.get() to retrieve credentials, keeping all authentication server-side.
Prerequisites
- 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
Register an OAuth2 Server-Based Application in Zoho Developer Console
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.
Pro tip: Note the 'api_domain' field in the token exchange response — it is the correct base URL for all your CRM API calls (e.g., https://www.zohoapis.com or https://www.zohoapis.eu). Store this as ZOHO_CRM_BASE_URL in secrets rather than hardcoding a regional URL.
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.
Store all OAuth2 credentials in Cloud Secrets
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).
Pro tip: Zoho CRM's module name for what other CRMs call 'Opportunities' is 'Deals' (or 'Potentials' in older API docs). In API v2 paths, use /crm/v2/Deals — not /Opportunities or /Potentials. The web UI may show different labels depending on your CRM settings.
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.
Create the Zoho CRM API proxy Edge Function with token refresh
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.
Create an Edge Function called zoho-crm at supabase/functions/zoho-crm/index.ts. Accept POST with { action, module, params }. Support actions: create_record, search_records, get_record, update_record. Use ZOHO_CRM_ACCESS_TOKEN, ZOHO_CRM_REFRESH_TOKEN, ZOHO_CRM_CLIENT_ID, ZOHO_CRM_CLIENT_SECRET, and ZOHO_CRM_BASE_URL from secrets. Implement automatic token refresh on 401. Use 'Zoho-oauthtoken' as the Authorization header. Include CORS headers.
Paste this in Lovable chat
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'23const corsHeaders = {4 'Access-Control-Allow-Origin': '*',5 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',6}78async function refreshToken(): Promise<string> {9 const baseUrl = Deno.env.get('ZOHO_CRM_BASE_URL')!10 const accountsDomain = baseUrl.replace('www.zohoapis', 'accounts.zoho')11 const resp = await fetch(`${accountsDomain}/oauth/v2/token`, {12 method: 'POST',13 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },14 body: new URLSearchParams({15 grant_type: 'refresh_token',16 client_id: Deno.env.get('ZOHO_CRM_CLIENT_ID')!,17 client_secret: Deno.env.get('ZOHO_CRM_CLIENT_SECRET')!,18 refresh_token: Deno.env.get('ZOHO_CRM_REFRESH_TOKEN')!,19 }),20 })21 const data = await resp.json()22 if (!data.access_token) throw new Error('Token refresh failed')23 return data.access_token24}2526async function crmFetch(path: string, method = 'GET', body?: unknown, retry = true): Promise<unknown> {27 const base = Deno.env.get('ZOHO_CRM_BASE_URL')!28 let token = Deno.env.get('ZOHO_CRM_ACCESS_TOKEN')!2930 let resp = await fetch(`${base}/crm/v2${path}`, {31 method,32 headers: {33 Authorization: `Zoho-oauthtoken ${token}`,34 'Content-Type': 'application/json',35 },36 body: body ? JSON.stringify(body) : undefined,37 })3839 if (resp.status === 401 && retry) {40 token = await refreshToken()41 resp = await fetch(`${base}/crm/v2${path}`, {42 method,43 headers: {44 Authorization: `Zoho-oauthtoken ${token}`,45 'Content-Type': 'application/json',46 },47 body: body ? JSON.stringify(body) : undefined,48 })49 }50 return resp.json()51}5253serve(async (req) => {54 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })5556 try {57 const { action, module: mod = 'Leads', params = {} } = await req.json()58 let result5960 switch (action) {61 case 'create_record':62 result = await crmFetch(`/${mod}`, 'POST', { data: [params] })63 break64 case 'search_records':65 result = await crmFetch(`/${mod}/search?criteria=${encodeURIComponent(params.criteria)}&per_page=${params.perPage || 20}`)66 break67 case 'get_record':68 result = await crmFetch(`/${mod}/${params.recordId}`)69 break70 case 'update_record':71 result = await crmFetch(`/${mod}/${params.recordId}`, 'PUT', { data: [params.fields] })72 break73 default:74 return new Response(JSON.stringify({ error: 'Unknown action' }), {75 status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },76 })77 }7879 return new Response(JSON.stringify(result), {80 headers: { ...corsHeaders, 'Content-Type': 'application/json' },81 })82 } catch (error) {83 return new Response(JSON.stringify({ error: error.message }), {84 status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },85 })86 }87})Pro tip: Zoho CRM field API names differ from display labels. For example, the 'Last Name' field is 'Last_Name' in the API. Check field API names in Zoho CRM → Settings → Modules and Fields → select module → field list. Using display labels instead of API names causes silent field misses.
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.
Build CRM-powered features in your Lovable app
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.
Build a CRM dashboard page that fetches recent Deals from Zoho CRM using the zoho-crm Edge Function (action: search_records, module: Deals). Show deals in a table with columns: Deal Name, Account Name, Stage, Amount, Close Date. Add a Create Lead button that opens a form with name, email, company, and source fields — on submit, call create_record with module Leads. Show a success toast when the lead is created.
Paste this in Lovable chat
Pro tip: Zoho CRM API v2 returns a 'INVALID_TOKEN' error code (not a standard 401) in some error scenarios. Add a check for response.data?.[0]?.code === 'INVALID_TOKEN' as an additional trigger for token refresh, alongside the HTTP 401 status check.
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.
Common 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.
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.
Copy this prompt to try it in Lovable
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.
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.
Copy this prompt to try it in Lovable
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.
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.
Copy this prompt to try it in Lovable
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.
1// Correct search criteria format:2const criteria = encodeURIComponent(`(Email:equals:${email})`)3const 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).
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.
Alternatives
Choose Zoho Books if your integration need is accounting and invoicing rather than sales pipeline — both share the same OAuth2 authentication pattern and integrate natively with each other.
Choose Pipedrive if you want a standalone sales pipeline CRM with a simpler API (API token, no OAuth2) and strong visual pipeline management without the Zoho ecosystem dependency.
Choose HubSpot if you need marketing automation alongside CRM and want a more generous free tier — HubSpot's CRM is free forever with no user limit, while Zoho CRM's free plan is limited to 3 users.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation