Skip to main content
RapidDev - Software Development Agency
v0-integrationsNext.js API Route

How to Integrate Zoho CRM with V0

To integrate Zoho CRM with a V0 by Vercel app, create a Next.js API route that authenticates via Zoho's OAuth2 client credentials flow and calls the Zoho CRM v3 REST API. Generate your lead capture and CRM dashboard UI with V0, add an API route that creates contacts and fetches deals, and store your Zoho OAuth credentials in Vercel environment variables. This lets you sync leads from V0 forms directly into Zoho CRM.

What you'll learn

  • How to generate CRM lead capture forms and dashboard UI using V0 prompts
  • How to set up Zoho CRM OAuth2 server-to-server authentication for a Next.js app
  • How to create a Next.js API route that creates contacts and fetches CRM records from Zoho
  • How to store Zoho OAuth credentials securely as Vercel environment variables
  • How to handle OAuth2 token refresh for long-running CRM integrations
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read60 minutesMarketingMarch 2026RapidDev Engineering Team
TL;DR

To integrate Zoho CRM with a V0 by Vercel app, create a Next.js API route that authenticates via Zoho's OAuth2 client credentials flow and calls the Zoho CRM v3 REST API. Generate your lead capture and CRM dashboard UI with V0, add an API route that creates contacts and fetches deals, and store your Zoho OAuth credentials in Vercel environment variables. This lets you sync leads from V0 forms directly into Zoho CRM.

Sync Leads and Manage CRM Data with Zoho CRM and V0

Zoho CRM is one of the most popular CRM platforms for small and mid-sized businesses, offering competitive pricing compared to Salesforce or HubSpot while providing a full feature set: contact management, deal pipelines, email automation, reports, and a robust REST API. For V0 builders targeting businesses on a budget, Zoho CRM is often the CRM of choice.

The integration pattern is common for marketing apps: V0 generates your landing page with lead capture form, and when a visitor submits their information, a Next.js API route creates a contact and lead record in Zoho CRM. Your sales team sees new leads in real time without manually entering data. You can also build CRM dashboards that display current pipeline data from Zoho — deal stages, follow-up tasks, and contact activity.

Zoho CRM uses OAuth2 for API authentication. For server-to-server integrations (a single CRM account accessed by your app), the Self Client authorization approach with a refresh token is most appropriate. You generate a refresh token once through Zoho's developer console, store it as an environment variable, and your API route exchanges it for access tokens on demand. Access tokens expire after an hour, so the refresh token flow is essential for production applications.

Integration method

Next.js API Route

Zoho CRM integrates with V0 apps through Next.js API routes that use Zoho's OAuth2 server-to-server flow. A Next.js API route exchanges your client credentials for an access token, then calls Zoho CRM's v3 REST API to create contacts, fetch leads, and update deal stages. Access tokens are short-lived and refreshed automatically using the refresh token stored in environment variables.

Prerequisites

  • A Zoho CRM account — free tier available at zoho.com/crm (up to 3 users)
  • A Zoho developer account at api-console.zoho.com to create your OAuth application
  • Your Zoho OAuth Client ID, Client Secret, and a Refresh Token (generated via Self Client flow)
  • Your Zoho data center URL (api.zoho.com for US, api.zoho.eu for EU, api.zoho.in for India)
  • A V0 project exported to GitHub and deployed on Vercel

Step-by-step guide

1

Create a Zoho OAuth Application and Get Credentials

Navigate to Zoho's API Console at api-console.zoho.com. Click 'Add Client' and select 'Server-based Applications' as the client type. Enter a name like 'My V0 App', set the Authorized Redirect URI to `https://your-app.vercel.app/api/auth/zoho/callback` (you'll also add localhost for development), and click 'Create'. After creating the client, you'll see your Client ID and Client Secret. Copy both — you'll add them to Vercel environment variables. These credentials identify your application when making OAuth requests. For server-to-server integration (your app accessing your own CRM, not your users' CRMs), the recommended approach is generating a refresh token once and storing it. In the Zoho API Console, click on your app, then go to the 'Self Client' tab. Enter the required OAuth scopes for CRM access: `ZohoCRM.modules.ALL` for full access, or more specific scopes like `ZohoCRM.modules.Leads.CREATE,ZohoCRM.modules.Contacts.ALL` for limited access. Set the time duration to '10 minutes' and click 'Create'. Zoho generates a one-time authorization code. To exchange this code for a refresh token, make a POST request to `https://accounts.zoho.com/oauth/v2/token` with parameters: `code={your_code}`, `client_id={your_client_id}`, `client_secret={your_client_secret}`, `redirect_uri=https://your-app.vercel.app/api/auth/zoho/callback`, `grant_type=authorization_code`. The response includes a `refresh_token` — save this value, you won't see it again. This refresh token doesn't expire (unless you revoke it) and is what your API route will use to generate fresh access tokens.

V0 Prompt

Create a lead capture form page with the following fields: First Name, Last Name, Company, Email (required), Phone, Job Title, How did you hear about us (dropdown: Google, LinkedIn, Referral, Other), and a message textarea. Add a prominent submit button. Show a success state with a thank you message and a 'Submit another lead' link. Include client-side validation with inline error messages.

Paste this in V0 chat

Pro tip: Store the refresh token immediately after generating it — Zoho only shows it once. If you lose it, you'll need to revoke the access and generate a new authorization code to get a new refresh token.

Expected result: Zoho OAuth Client ID, Client Secret, and Refresh Token obtained from api-console.zoho.com. Scopes include ZohoCRM.modules.Leads.CREATE and ZohoCRM.modules.Contacts.ALL.

2

Create the Zoho CRM API Route

Create a Next.js API route at `app/api/zoho/route.ts` that handles both token refresh and Zoho CRM API calls. The OAuth2 token refresh flow works as follows: your route calls Zoho's token endpoint with the refresh token to get a new access token (valid for 1 hour), then uses that access token in the Authorization header for Zoho CRM API calls. Zoho's CRM v3 API base URL depends on your data center: `https://www.zohoapis.com/crm/v3/` for US, `https://www.zohoapis.eu/crm/v3/` for Europe, `https://www.zohoapis.in/crm/v3/` for India. Store this as an environment variable. For creating leads, POST to `/crm/v3/Leads` with the lead data in a `data` array. Zoho's API wraps all records in an array even when creating a single record — this is the most common mistake developers make. The response includes the created record ID and status. For fetching contacts and deals, use GET requests to `/crm/v3/Contacts` and `/crm/v3/Deals`. Add `fields` parameter to specify which fields to return (avoiding fetching all 100+ CRM fields when you only need 5). Use `sort_by` and `sort_order` for ordering, and `page` with `per_page` for pagination. To avoid making a token refresh call on every API request (which wastes time and counts against Zoho's token call limits), cache the access token in a module-level variable with its expiry time. If the cached token is still valid (more than 5 minutes remaining), reuse it. Only refresh when the token is expired or about to expire.

app/api/zoho/route.ts
1// app/api/zoho/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID!;
5const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET!;
6const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN!;
7const ZOHO_API_BASE = process.env.ZOHO_API_BASE || 'https://www.zohoapis.com/crm/v3';
8const ZOHO_ACCOUNTS_URL = process.env.ZOHO_ACCOUNTS_URL || 'https://accounts.zoho.com';
9
10// Module-level token cache
11let cachedToken: { token: string; expiresAt: number } | null = null;
12
13async function getAccessToken(): Promise<string> {
14 if (cachedToken && Date.now() < cachedToken.expiresAt - 60_000) {
15 return cachedToken.token;
16 }
17 const res = await fetch(`${ZOHO_ACCOUNTS_URL}/oauth/v2/token`, {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
20 body: new URLSearchParams({
21 refresh_token: ZOHO_REFRESH_TOKEN,
22 client_id: ZOHO_CLIENT_ID,
23 client_secret: ZOHO_CLIENT_SECRET,
24 grant_type: 'refresh_token',
25 }),
26 });
27 const data = await res.json();
28 if (!data.access_token) throw new Error(`Token refresh failed: ${JSON.stringify(data)}`);
29 cachedToken = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
30 return data.access_token;
31}
32
33async function zohoFetch(method: string, path: string, body?: object) {
34 const token = await getAccessToken();
35 const res = await fetch(`${ZOHO_API_BASE}${path}`, {
36 method,
37 headers: {
38 Authorization: `Zoho-oauthtoken ${token}`,
39 'Content-Type': 'application/json',
40 },
41 body: body ? JSON.stringify(body) : undefined,
42 });
43 return res.json();
44}
45
46export async function POST(request: NextRequest) {
47 const { resource, data } = await request.json();
48 if (!resource || !data) return NextResponse.json({ error: 'resource and data required' }, { status: 400 });
49
50 try {
51 // Zoho requires records wrapped in { data: [] } array
52 const result = await zohoFetch('POST', `/${resource}`, { data: [data] });
53 return NextResponse.json(result);
54 } catch (err: any) {
55 return NextResponse.json({ error: err.message }, { status: 500 });
56 }
57}
58
59export async function GET(request: NextRequest) {
60 const { searchParams } = new URL(request.url);
61 const resource = searchParams.get('resource') || 'Contacts';
62 const fields = searchParams.get('fields') || 'id,Full_Name,Email,Phone,Lead_Status';
63 const page = searchParams.get('page') || '1';
64
65 try {
66 const result = await zohoFetch('GET', `/${resource}?fields=${fields}&page=${page}&per_page=50`);
67 return NextResponse.json(result);
68 } catch (err: any) {
69 return NextResponse.json({ error: err.message }, { status: 500 });
70 }
71}

Pro tip: Module-level token caching works in development and for single-instance serverless deployments. For high-traffic apps with multiple serverless function instances, consider caching the token in Upstash Redis (available as a Vercel Marketplace integration) to share the cache across all instances.

Expected result: The API route successfully refreshes the Zoho access token and can create leads via POST and fetch contacts via GET.

3

Configure Vercel Environment Variables

Navigate to your Vercel project → Settings → Environment Variables. Add the following variables, all as server-only (no NEXT_PUBLIC_ prefix). Add `ZOHO_CLIENT_ID` — the Client ID from your Zoho API Console application. Add `ZOHO_CLIENT_SECRET` — the Client Secret from the same application. Add `ZOHO_REFRESH_TOKEN` — the refresh token you generated in the Self Client flow. This is the most sensitive credential — treat it like a password and never commit it to Git. Add `ZOHO_API_BASE` — the Zoho CRM API base URL for your data center. US customers use `https://www.zohoapis.com/crm/v3`. European customers use `https://www.zohoapis.eu/crm/v3`. Indian customers use `https://www.zohoapis.in/crm/v3`. If you're unsure which data center you're on, log into Zoho CRM and check the URL — it will show your region. Add `ZOHO_ACCOUNTS_URL` — the Zoho accounts URL for token refresh. US: `https://accounts.zoho.com`, EU: `https://accounts.zoho.eu`, India: `https://accounts.zoho.in`. This must match your data center. After saving all variables, redeploy the application. Test the integration by making a POST request to `/api/zoho` with `{ resource: 'Leads', data: { Last_Name: 'Test', Email: 'test@example.com', Company: 'Test Co' } }` and verify the lead appears in your Zoho CRM. For EU GDPR compliance and other regional requirements, ensure all data is routed to the correct Zoho data center. Using the US endpoint from a EU-based app violates GDPR data residency requirements.

Pro tip: If your refresh token stops working after a few months, check if you've connected more than one app with the same scopes in Zoho's API Console — Zoho limits active refresh tokens per scope combination and revokes older ones when the limit is reached.

Expected result: All five Zoho environment variables set in Vercel with the correct data center URLs, triggering a successful test lead creation in Zoho CRM.

4

Connect the Lead Form to Zoho CRM

Update the V0-generated lead capture form to submit data to your Zoho API route. Map form fields to Zoho CRM's field API names — Zoho's field names use specific formats that differ from display names. Key Zoho CRM Lead field API names: `Last_Name` (required for Leads module), `First_Name`, `Email`, `Phone`, `Company` (required), `Lead_Source` (dropdown — values must match Zoho CRM's picklist: 'Web Site', 'Cold Call', 'Employee Referral', 'Partner', etc.), `Lead_Status` (default: 'Not Contacted'), `Description`. In your form submit handler, construct the Zoho data object with correct field names and POST to `/api/zoho`. Handle the response — Zoho returns a `data` array with status objects. A successful creation shows `{ code: 'SUCCESS', status: 'success', id: '...' }`. Handle duplicate records — if a lead with the same email exists, Zoho may return a duplicate error depending on your CRM's duplicate check settings. For additional automation value, add UTM parameter tracking to form submissions. Read UTM parameters from the URL (`document.referrer`, `window.location.search`) and include them in the lead's `Lead_Source` or custom fields. This tells your sales team exactly which campaign or ad drove each lead. For the CRM dashboard view, fetch leads from `/api/zoho?resource=Leads&fields=id,First_Name,Last_Name,Email,Company,Lead_Source,Lead_Status,Created_Time` and map them to table rows or Kanban cards. The dashboard should show each lead's status in the pipeline and allow clicking through to view more detail.

V0 Prompt

Update the lead capture form to POST to /api/zoho on submit. Map the form data to Zoho field names: firstName → First_Name, lastName → Last_Name, email → Email, phone → Phone, company → Company, message → Description. On success (response.data[0].status === 'success'), show a thank you message with the lead ID. On error or duplicate, show a helpful error message. Add a loading spinner on the submit button while the API call is in progress.

Paste this in V0 chat

lib/zoho-helpers.ts
1// Form submit handler for Zoho lead creation
2async function handleFormSubmit(formData: {
3 firstName: string;
4 lastName: string;
5 email: string;
6 phone: string;
7 company: string;
8 message: string;
9 leadSource: string;
10}) {
11 const zohoLead = {
12 First_Name: formData.firstName,
13 Last_Name: formData.lastName,
14 Email: formData.email,
15 Phone: formData.phone,
16 Company: formData.company,
17 Description: formData.message,
18 Lead_Source: formData.leadSource,
19 Lead_Status: 'Not Contacted',
20 };
21
22 const res = await fetch('/api/zoho', {
23 method: 'POST',
24 headers: { 'Content-Type': 'application/json' },
25 body: JSON.stringify({ resource: 'Leads', data: zohoLead }),
26 });
27
28 const result = await res.json();
29 const status = result.data?.[0];
30
31 if (status?.status === 'success') {
32 return { success: true, id: status.details?.id };
33 } else if (status?.code === 'DUPLICATE_DATA') {
34 return { success: false, error: 'A contact with this email already exists.' };
35 } else {
36 return { success: false, error: 'Failed to submit. Please try again.' };
37 }
38}

Pro tip: Zoho CRM Lead_Source must be one of the values in your CRM's picklist — check Zoho CRM → Settings → Modules → Leads → Fields → Lead Source to see the exact available values and ensure your form dropdown matches.

Expected result: Form submissions create real Lead records in Zoho CRM with all fields mapped correctly. Duplicate submissions show an appropriate error message.

Common use cases

Lead Capture Form to Zoho CRM

Build a landing page with a contact form that automatically creates a new Lead record in Zoho CRM when submitted. V0 generates the form UI with validation, and the API route creates the lead with all form data including source tracking.

V0 Prompt

Create a lead capture landing page with a form: first name, last name, email, phone, company name, job title, and a 'How can we help?' textarea. Add a 'Get Started' submit button with loading state. On success, show a thank you message. Include field validation (email format, required fields).

Copy this prompt to try it in V0

Sales Pipeline Dashboard

Build an internal sales dashboard that shows the current deal pipeline from Zoho CRM — deals by stage, total pipeline value, recent activity, and team performance metrics. The API route fetches deal data from Zoho and the V0-generated components display it in a visual pipeline board.

V0 Prompt

Create a sales pipeline dashboard with a Kanban board showing deal stages as columns (Qualification, Proposal, Negotiation, Closed Won, Closed Lost). Each deal card shows company name, deal value, close date, and owner initials. Show total pipeline value at the top. Add a deals table below the board with sortable columns.

Copy this prompt to try it in V0

Customer Portal with CRM Data

Build a customer-facing portal where signed-in customers can see their account details, recent interactions, and open support cases — all pulled from Zoho CRM contact records in real time. The API route fetches the contact record matching the logged-in user's email.

V0 Prompt

Create a customer account page with sections: (1) account summary showing company name, account manager name, and relationship start date, (2) recent activities timeline showing last 5 call/email notes, (3) open tasks list, (4) a contact form to message the account manager. All data loaded from the CRM API.

Copy this prompt to try it in V0

Troubleshooting

Token refresh returns error: 'invalid_code' or 'invalid_client'

Cause: The ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, or ZOHO_REFRESH_TOKEN environment variable has an incorrect value, or the refresh token was generated for a different Zoho data center than ZOHO_ACCOUNTS_URL.

Solution: Verify all three credentials by testing the token refresh manually with curl or Postman. Ensure ZOHO_ACCOUNTS_URL matches your Zoho data center — using accounts.zoho.com for an EU account (which needs accounts.zoho.eu) is a common mistake.

typescript
1// Test token refresh manually:
2// POST https://accounts.zoho.com/oauth/v2/token
3// Body (x-www-form-urlencoded):
4// refresh_token=YOUR_REFRESH_TOKEN
5// client_id=YOUR_CLIENT_ID
6// client_secret=YOUR_CLIENT_SECRET
7// grant_type=refresh_token

Lead creation returns 'INVALID_DATA' with 'the field Last_Name is required'

Cause: The Zoho CRM Leads module requires Last_Name as a mandatory field. Other modules (Contacts) require different fields.

Solution: Always include Last_Name when creating Leads. When creating Contacts, the required fields are Last_Name and Account_Name. Check Zoho CRM's module settings for required fields specific to your CRM configuration.

typescript
1// Minimum required for Lead creation:
2const lead = {
3 Last_Name: 'Doe', // REQUIRED for Leads
4 Company: 'Acme Corp', // REQUIRED for Leads
5 Email: 'john@acme.com' // not required but highly recommended
6};

API returns 401 'AUTHENTICATION_FAILURE' after initially working

Cause: The cached access token has expired (tokens last 1 hour) and the refresh is failing, or the module-level cache was reset by a cold start in Vercel's serverless environment.

Solution: The module-level cache approach requires the getAccessToken function to handle expired tokens gracefully. Ensure the expiry check uses sufficient buffer time (5 minutes). If this is a recurring issue in production, implement external caching with Upstash Redis.

typescript
1// Add more buffer time to expiry check
2if (cachedToken && Date.now() < cachedToken.expiresAt - 300_000) { // 5 min buffer
3 return cachedToken.token;
4}

Zoho API returns 'LIMIT_EXCEEDED' errors

Cause: Zoho CRM has API call limits based on your plan: Free has 1,000 calls/day, Standard has 5,000/day, Professional has 10,000/day.

Solution: Implement caching for read operations using Next.js fetch revalidation. Avoid making API calls on every page render — cache list data for at least 5-15 minutes. For the free plan, prioritize write operations (lead creation) over reads.

typescript
1// Cache read operations
2const result = await fetch('/api/zoho?resource=Contacts', {
3 next: { revalidate: 300 } // cache for 5 minutes
4});

Best practices

  • Cache OAuth2 access tokens in a module-level variable with expiry tracking to avoid refreshing on every API call
  • Use the correct Zoho data center for token refresh and API calls — mismatched data centers are a common integration failure
  • Always wrap Zoho CRM record creation in a try-catch and handle duplicate detection gracefully — duplicate records corrupt CRM data
  • Use specific field selectors in GET requests rather than fetching all fields — Zoho CRM modules can have 100+ fields and only 5-10 may be relevant to your UI
  • Map form Lead_Source values to exact Zoho CRM picklist values — mismatched values cause INVALID_DATA errors
  • Store the Zoho refresh token securely and never commit it to Git — treat it as a password that grants full CRM access
  • Log successful and failed lead creation events to your application's audit log — useful for debugging data quality issues with the sales team

Alternatives

Frequently asked questions

Does Zoho CRM have a free API tier?

Yes, Zoho CRM's free plan includes API access with a limit of 1,000 API calls per day (per organization). This is sufficient for low-volume lead capture forms but may be insufficient for high-traffic sites or complex dashboard applications. Paid plans (Standard at $14/user/month) increase the limit to 5,000 calls/day, and Professional ($23/user/month) includes 10,000 calls/day.

What is the difference between creating a Lead vs a Contact in Zoho CRM?

In Zoho CRM's default configuration, Leads are unqualified prospects that haven't been vetted by the sales team. Contacts are qualified individuals associated with an Account (company). The typical workflow is: website form submission creates a Lead → sales team qualifies it → Lead is converted to Contact + Account + Deal. Your lead capture form should POST to the Leads module. The Contacts module is for CRM data you already have confirmed relationships with.

How do I handle the case where a lead's email already exists in Zoho CRM?

Zoho CRM returns a DUPLICATE_DATA error code in the response `data[0].code` when the submitted record duplicates an existing one (based on your CRM's duplicate detection settings). In your form submit handler, catch this code and display a user-friendly message like 'We already have your information — our team will be in touch soon!' to avoid alarming the user with a technical error.

Can I sync Zoho CRM data to my V0 app in real time?

Yes, Zoho CRM supports webhooks that notify your app when records are created or updated. In Zoho CRM settings, go to Setup → Automation → Notifications → Webhooks and configure a webhook pointing to a POST endpoint in your Next.js app (e.g., `/api/zoho/webhook`). Your webhook handler receives JSON payloads when leads are updated, deals change stage, or contacts are modified.

Does this integration work with Zoho One bundle accounts?

Yes, Zoho One accounts include Zoho CRM and all API access follows the same OAuth2 flow. Your Self Client application in the Zoho API Console works the same way regardless of whether you're on a standalone CRM plan or Zoho One. The API quotas may differ based on the Zoho CRM edition included in your Zoho One subscription.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.