Connect FlutterFlow to Zoho CRM v7 using the REST API. Because the OAuth refresh token is a server secret, a Firebase Cloud Function handles token refresh and returns short-lived access tokens to FlutterFlow. The API Group uses the `Zoho-oauthtoken` header (not Bearer) and region-specific base URLs — pick the wrong region domain and every call 401s.
| Fact | Value |
|---|---|
| Tool | Zoho CRM |
| Category | CRM & Sales |
| Method | FlutterFlow API Call |
| Difficulty | Advanced |
| Time required | 60 minutes |
| Last updated | July 2026 |
Connecting FlutterFlow to Zoho CRM: Regions, Tokens, and the `data` Array
Zoho CRM v7 exposes a clean REST API for its Leads, Contacts, Deals, and Accounts modules. The two details that trip up almost every FlutterFlow integration are the region-specific domain and the non-standard authorization header. Zoho runs data centers in the US (zohoapis.com), EU (.eu), India (.in), and Australia (.au) — calling the wrong domain returns 401 or INVALID_TOKEN errors even when your credentials are correct. The authorization header is `Authorization: Zoho-oauthtoken [token]`, not the standard `Bearer` prefix; tools that auto-generate REST calls often use Bearer by default, which fails silently with Zoho.
Beyond those two quirks, the FlutterFlow architecture requires a security layer. Zoho CRM uses OAuth 2.0 with a self-client pattern: you generate a grant code in the Zoho API Console, exchange it for a refresh token, and that refresh token is long-lived. It is a server-side credential that cannot be embedded in a compiled Flutter app. A Firebase Cloud Function holds the refresh token and calls Zoho's token endpoint to get a 60-minute access token whenever FlutterFlow asks.
Zoho CRM offers a free tier for up to 3 users; paid plans start at around $14 per user per month on the Standard tier (verify current pricing at zoho.com/crm/pricing.html). API call limits are credit-based and tier-dependent — lower plans receive approximately 3,000 API calls per hour shared across the organization, while higher plans go up to 10,000 per hour. A ListView that auto-refreshes every few seconds on multiple devices will exhaust this budget quickly; prefer on-demand fetching and App State caching instead.
Integration method
FlutterFlow queries Zoho CRM v7 REST modules using API Calls with the `Authorization: Zoho-oauthtoken [token]` header. The OAuth self-client refresh token is a long-lived server secret that cannot live inside the compiled Flutter app — a Firebase Cloud Function holds it and returns a fresh 1-hour access token on demand. FlutterFlow stores the access token in App State and injects it into every API Call to Leads, Contacts, and Deals endpoints.
Prerequisites
- A Zoho CRM account — free tier (up to 3 users) or a paid plan, with access to the Zoho API Console at api-console.zoho.com
- Knowledge of which Zoho data center your account uses (US, EU, India, or Australia) — visible in your Zoho account URL
- A Firebase project on the Blaze (pay-as-you-go) plan with Cloud Functions enabled
- A FlutterFlow project on at least the Starter plan to access API Calls and App State
- Basic familiarity with FlutterFlow's left-nav panels, API Calls section, and App Values
Step-by-step guide
Register a Zoho self-client and generate a refresh token
Go to api-console.zoho.com and sign in with your Zoho account. Click '+ Add Client' and choose 'Self Client' from the client type options — this is Zoho's server-to-server OAuth option designed for backend integrations where there is no interactive user login. Once the self-client is created, you'll see a Client ID and Client Secret in the client details panel. Copy both — the Client Secret is a server credential and goes into your Firebase Cloud Function, not into FlutterFlow. To generate the initial refresh token, click the 'Generate Code' tab in the self-client panel. In the Scope field, enter the scopes your FlutterFlow app needs — for CRM access, use `ZohoCRM.modules.leads.CREATE,ZohoCRM.modules.leads.READ,ZohoCRM.modules.deals.READ,ZohoCRM.modules.contacts.READ`. Set the Time Duration to the maximum (e.g. 10 minutes). Click 'Create' and copy the generated grant code immediately — it expires in the time you chose. Now exchange this grant code for a refresh token by making a POST request to Zoho's token endpoint. You can do this from any HTTP client (the Postman web app or even your browser's fetch console): POST to `https://accounts.zoho.com/oauth/v2/token` (use `.eu`, `.in`, or `.com.au` for other regions) with form body params `grant_type=authorization_code`, `client_id=[your client id]`, `client_secret=[your client secret]`, and `code=[your grant code]`. The response includes a `refresh_token` — copy it. This is the long-lived secret that goes into Firebase. The initial `access_token` in this response expires in 3,600 seconds.
1// Exchange grant code for refresh token (run once, outside FlutterFlow)2POST https://accounts.zoho.com/oauth/v2/token3Content-Type: application/x-www-form-urlencoded45grant_type=authorization_code6&client_id=YOUR_CLIENT_ID7&client_secret=YOUR_CLIENT_SECRET8&code=YOUR_GRANT_CODE9&redirect_uri=10Pro tip: The grant code is one-time use and expires in minutes — have your Firebase Cloud Function code ready to paste and the function environment variables filled in before you generate it, so you can test the token exchange immediately.
Expected result: The token exchange response contains a `refresh_token` field — a long opaque string. Store this securely; it does not expire automatically and is the root credential for all subsequent token refreshes.
Deploy a Firebase Cloud Function for Zoho token refresh
Open your Firebase project in the Firebase Console. Navigate to 'Build → Functions'. The Cloud Function will hold your Zoho Client ID, Client Secret, and Refresh Token — the three values that cannot go inside the Flutter app. Create the function code shown below. It receives an HTTPS GET request from FlutterFlow, calls Zoho's region-appropriate token endpoint with the stored credentials, and returns a fresh access token as JSON. The function uses Firebase environment config to store secrets: in your local Firebase CLI terminal, run `firebase functions:config:set zoho.client_id="..." zoho.client_secret="..." zoho.refresh_token="..." zoho.region="com"` (use `eu`, `in`, or `com.au` instead of `com` for non-US accounts). After deploying, copy the function's HTTPS trigger URL — this is what FlutterFlow will call on app launch. Note: CORS headers are included in the function so that FlutterFlow web builds (running in a browser) can call it. Mobile builds don't need CORS but the headers don't hurt.
1const functions = require('firebase-functions');2const axios = require('axios');34exports.getZohoCRMToken = functions.https.onRequest(async (req, res) => {5 res.set('Access-Control-Allow-Origin', '*');6 if (req.method === 'OPTIONS') {7 res.set('Access-Control-Allow-Methods', 'GET');8 res.set('Access-Control-Allow-Headers', 'Content-Type');9 return res.status(204).send('');10 }1112 const config = functions.config().zoho;13 const tokenUrl = `https://accounts.zoho.${config.region}/oauth/v2/token`;1415 try {16 const params = new URLSearchParams();17 params.append('refresh_token', config.refresh_token);18 params.append('client_id', config.client_id);19 params.append('client_secret', config.client_secret);20 params.append('grant_type', 'refresh_token');2122 const response = await axios.post(tokenUrl, params);23 return res.json({24 access_token: response.data.access_token,25 expires_in: response.data.expires_in26 });27 } catch (err) {28 console.error('Zoho token error:', err.response?.data || err.message);29 return res.status(500).json({ error: 'Token refresh failed' });30 }31});32Pro tip: The token URL must match your Zoho data center: `accounts.zoho.com` for US, `accounts.zoho.eu` for EU, `accounts.zoho.in` for India, `accounts.zoho.com.au` for Australia. A domain mismatch causes 401 even with correct credentials.
Expected result: Calling the Cloud Function URL in a browser returns `{"access_token": "...", "expires_in": 3600}`. The token starts with '1000' and is several hundred characters long.
Create the Zoho CRM v7 API Group in FlutterFlow
Open FlutterFlow and go to the left nav → API Calls → '+ Add' → 'Create API Group'. Name it 'ZohoCRM'. Set the Base URL to your region's Zoho APIs endpoint: `https://www.zohoapis.com/crm/v7` for US, or `https://www.zohoapis.eu/crm/v7`, `https://www.zohoapis.in/crm/v7`, or `https://www.zohoapis.com.au/crm/v7` for other regions. This must match the region you used for your token endpoint in the Cloud Function. Under 'Headers', add a shared header at the API Group level: key `Authorization`, value `Zoho-oauthtoken [accessToken]`. Note the exact prefix: it is `Zoho-oauthtoken`, not `Bearer`. This is non-standard and a very common mistake. Add a Variable named `accessToken` of type String, then set the Authorization header value to `Zoho-oauthtoken [accessToken]` using FlutterFlow's variable interpolation. Now add individual API Calls inside the ZohoCRM group: 1. 'getLeads' — Method GET, endpoint `/Leads?fields=Full_Name,Email,Phone,Company,Lead_Status&per_page=50`. Parse the response JSON path `$.data` in the Response & Test tab. 2. 'createLead' — Method POST, endpoint `/Leads`. Add a JSON body with variables: `firstName`, `lastName`, `email`, `phone`, `company`. The POST body must wrap the record in the `data` array: `{"data":[{"First_Name":"[firstName]","Last_Name":"[lastName]","Email":"[email]","Phone":"[phone]","Company":"[company]"}]}`. 3. 'getDeals' — Method GET, endpoint `/Deals?fields=Deal_Name,Stage,Amount,Closing_Date&per_page=50`. Parse `$.data`. In the Response & Test tab for getLeads, paste a sample Zoho CRM response (a JSON with a `data` array containing lead objects) and click 'Generate JSON Paths' to auto-create binding paths.
1{2 "method": "POST",3 "endpoint": "/Leads",4 "headers": {5 "Authorization": "Zoho-oauthtoken [accessToken]",6 "Content-Type": "application/json"7 },8 "body": {9 "data": [10 {11 "First_Name": "[firstName]",12 "Last_Name": "[lastName]",13 "Email": "[email]",14 "Phone": "[phone]",15 "Company": "[company]",16 "Lead_Source": "Mobile App"17 }18 ]19 }20}21Pro tip: Zoho CRM module names in v7 API URLs are capitalized and match the CRM UI names exactly: `/Leads`, `/Contacts`, `/Deals`, `/Accounts`. Using `/leads` (lowercase) returns a 404 routing error.
Expected result: The FlutterFlow API Calls panel shows a 'ZohoCRM' group with getLeads, createLead, and getDeals calls. Testing getLeads in the Response & Test tab with a real access token returns 200 with a `data` array of lead records.
Fetch the access token on app launch and store it in App State
Create a second API Group in FlutterFlow called 'ZohoTokenService'. Set the Base URL to your Firebase Cloud Function's HTTPS trigger URL (e.g. `https://us-central1-yourproject.cloudfunctions.net`). Add one API Call named `getToken` with Method GET and endpoint path `/getZohoCRMToken`. No headers or body needed. Next, set up App State. Click the puzzle-piece icon in the left nav → 'App State'. Add a String variable named `zohoAccessToken` with an empty initial value. This variable holds the active access token throughout the session. Wire the token fetch to app launch. Click your root page (e.g. HomePage) in the left nav. Open the Actions panel → add an action to the 'On Page Load' trigger → choose 'Backend/API Call' → select `getToken` from ZohoTokenService. In the 'Action Output Variable' section, map `$.access_token` (the JSON path in the Cloud Function response) to the App State variable `zohoAccessToken`. Whenever you use a Zoho CRM API Call in an Action Flow, pass `App State → zohoAccessToken` as the `accessToken` argument. The API Group automatically injects it into the `Authorization: Zoho-oauthtoken` header. For sessions that last longer than an hour, add a 'Refresh Token' action to any page where the user might be active for extended periods. Wire a periodic refresh using FlutterFlow's Timer widget (if needed) or add a proactive token check on page focus events.
Pro tip: Zoho access tokens are valid for exactly 3,600 seconds (1 hour). If a user keeps the app open past that point and triggers an API call, they'll get INVALID_TOKEN. Store the token's creation timestamp in a second App State variable and compare it to the current time before each CRM call.
Expected result: App State → zohoAccessToken is populated with a non-empty string when the app starts. Passing this value to a getLeads API Call in an Action returns a 200 with CRM data.
Build a Leads ListView and a create-lead form
With the token flowing, build the CRM UI. On your main page, drag a ListView onto the canvas. Select it → open 'Backend Query' → set 'Query Type' to API Call → choose `getLeads` from the ZohoCRM group. Under 'Variables', pass `App State → zohoAccessToken` as `accessToken`. Under 'Response JSON Path', enter `$.data` so the ListView iterates over each lead in the data array. Inside the ListView, add a Column widget. Add Text widgets for the lead name (bind to `$.Full_Name`) and email (`$.Email`). Add a second Text for phone (`$.Phone`). You can also add a Badge or Chip widget for `$.Lead_Status` to visually indicate the lead stage. For the create-lead form, add a new page called 'NewLeadPage'. Add a Column with TextField widgets for First Name (variable `firstName`), Last Name (`lastName`), Email (`email`), Phone (`phone`), and Company (`company`). Add a Button labeled 'Save Lead'. In the Button's Actions panel: 1. Add a 'Backend/API Call' action → choose `createLead` from ZohoCRM group. 2. Map each TextField to the corresponding variable. 3. Pass `App State → zohoAccessToken` as `accessToken`. 4. After success (check status code 201), show a Snack Bar 'Lead saved to Zoho CRM' and navigate back. 5. After failure, show an Alert Dialog with the error code and message. Zoho CRM returns 201 Created for successful POSTs (not 200), so test for 201 in your conditional actions. The response body from a create includes `$.data[0].details.id` — the new Zoho record ID if you want to store it.
Pro tip: Zoho CRM field names in API v7 use underscores: `First_Name`, `Last_Name`, `Email`, `Phone`, `Company`, `Lead_Status`, `Lead_Source`. These are case-sensitive. Check the exact field API names in Zoho CRM → Settings → Developer Hub → API Names if you're unsure of a module's field names.
Expected result: The Leads ListView shows real records from Zoho CRM in FlutterFlow Run mode. Submitting the create-lead form produces a 201 response and the new lead appears in Zoho CRM Leads module within seconds.
Handle token expiry, region 401s, and per-hour API credit limits
Three runtime issues are common in production Zoho CRM integrations and worth handling explicitly in your FlutterFlow Action Flows. First, token expiry (INVALID_TOKEN / 401): in any Action Flow that calls a Zoho CRM endpoint, add a Conditional Action after the API Call. Check 'Status Code equals 401'. If true, call `getToken` from ZohoTokenService, update `App State → zohoAccessToken`, then re-execute the original API Call. This handles the hourly token expiry transparently. Second, region mismatch errors: if you move to a different Zoho data center or your organization migrates, you'll see `INVALID_TOKEN` even with a freshly minted token because the API domain no longer matches the token issuer. If this happens, verify your Zoho account's data center at zoho.com/accounts → profile → data center, then update both the Cloud Function's region config and the FlutterFlow API Group base URL to match. Third, API credit exhaustion: Zoho CRM limits API calls per hour on a credit-based model — approximately 3,000–10,000 per hour depending on your plan, shared across all users in the organization. A ListView that auto-refreshes every few seconds on multiple devices can exhaust this quickly. Instead, fetch data only on page load and on explicit user pull-to-refresh gestures. Cache the results in App State or Page State variables and use them to populate widgets without re-calling the API on every navigation event. If you'd rather not manage the proxy and token-refresh logic yourself, RapidDev's team builds and maintains FlutterFlow integrations like this routinely — book a free scoping call at rapidevelopers.com/contact.
Pro tip: Zoho CRM's COQL (CRM Object Query Language) lets you write SQL-style queries like `SELECT Full_Name, Email FROM Leads WHERE Lead_Status = 'New' LIMIT 50`. Use COQL for filtered or joined reads by POSTing to `/crm/v7/coql` with a JSON body `{"select_query": "..."}` — it's more powerful than query params for complex filters.
Expected result: When a 401 occurs mid-session, the app silently refreshes the token and retries. When API credits are near exhaustion, the app shows a friendly waiting message rather than an unhandled error.
Common use cases
Mobile lead-capture app — push form submissions directly into Zoho CRM Leads
A FlutterFlow app used at events or in the field lets staff enter prospect details into a form. On submit, the app POSTs a new Lead to Zoho CRM v7 in the correct `{"data":[{...}]}` format. The lead appears instantly in the sales team's Zoho CRM Leads module, complete with source tag and owner assignment.
Build a Flutter lead-capture form with fields for first name, last name, email, phone, and company. On submit, POST the data as a new Lead to Zoho CRM v7 and show a success screen with the option to capture another lead.
Copy this prompt to try it in FlutterFlow
Field sales pipeline view — browse and update Deals on mobile
A FlutterFlow app shows the current user's open Deals pulled from Zoho CRM, sorted by closing date. Sales reps can tap a Deal to view details and update the stage from a dropdown — the update is PATCHed back to Zoho CRM v7 in real time, keeping the pipeline current without opening a browser.
Build a Flutter mobile app that displays open Deals from Zoho CRM in a list sorted by closing date. Tapping a deal shows its details and lets the user change the Deal Stage from a dropdown, which updates the record in Zoho CRM.
Copy this prompt to try it in FlutterFlow
Contact lookup tool — search CRM contacts from a mobile device
A FlutterFlow app with a search bar calls the Zoho CRM Contacts module with a COQL query to find matching records by name or email. Results appear in a scrollable list; tapping a contact opens a detail card with phone, email, and last activity — useful for account managers before a call.
Build a Flutter contact search app that queries Zoho CRM Contacts by name using COQL. Display results in a list with contact name, email, and phone. Tapping a contact shows a full detail screen.
Copy this prompt to try it in FlutterFlow
Troubleshooting
Every API call returns 401 or `INVALID_TOKEN` even after a fresh token refresh
Cause: The base URL region of the Zoho API Group does not match the region of your Zoho account. For example, using `zohoapis.com` for an account hosted on `zoho.eu` data center will always fail.
Solution: Check your Zoho account's data center at accounts.zoho.com → your profile. Match the domain in both the FlutterFlow API Group base URL (e.g. `https://www.zohoapis.eu/crm/v7`) and the Firebase Cloud Function's token URL (e.g. `https://accounts.zoho.eu/oauth/v2/token`). Both must use the same regional suffix.
Authorization header with `Bearer` returns 401; `Zoho-oauthtoken` works
Cause: Zoho CRM's API does not accept the standard `Bearer` authorization prefix — it requires the proprietary `Zoho-oauthtoken` prefix exactly as written.
Solution: In the FlutterFlow API Group's shared headers, set the Authorization header value to exactly `Zoho-oauthtoken [accessToken]` (no colon, exact capitalization). Using `Bearer [accessToken]` or `zoho-oauthtoken` (wrong case) will return 401.
POST to `/Leads` returns 400 Bad Request — lead not created
Cause: The request body is not wrapped in the required `data` array format. Zoho CRM v7 requires all POST/PUT bodies to have the structure `{"data": [{...}]}` — a flat JSON object returns 400.
Solution: In the FlutterFlow createLead API Call's Body tab, ensure the body is `{"data":[{"First_Name":"[firstName]","Last_Name":"[lastName]","Email":"[email]","Company":"[company]"}]}`. The `data` key must be an array even when creating a single record. Also verify that required fields (`Last_Name`) are included — Zoho CRM rejects records missing mandatory fields with a validation error.
App stops receiving CRM data after one hour — getLeads returns empty or 401
Cause: Zoho access tokens expire after 3,600 seconds (1 hour). If the app does not re-fetch the token, all subsequent API calls fail.
Solution: In the Action Flow for every page where users might stay longer than an hour, add an 'On Page Load' action that calls the `getToken` Cloud Function and updates App State → zohoAccessToken. Alternatively, store the token creation timestamp in App State and check if more than 3,500 seconds have elapsed before each CRM API call, refreshing proactively if so.
Best practices
- Always match the Zoho API domain region (`zohoapis.com`, `.eu`, `.in`, `.com.au`) to your Zoho account's data center — this is the single most common cause of 401 errors in Zoho integrations.
- Keep the refresh token exclusively in the Firebase Cloud Function environment variables — never store it in FlutterFlow App State, App Values, or hard-code it in Dart code.
- Use the `Zoho-oauthtoken` authorization prefix exactly — it is case-sensitive and not interchangeable with `Bearer`, which Zoho silently rejects.
- Wrap all POST bodies in the `data` array format `{"data":[{...}]}` even for single-record inserts — omitting the array structure is the leading cause of 400 errors on write operations.
- Cache Zoho CRM list responses in App State or Page State and refresh them only on explicit user action (pull-to-refresh) rather than auto-polling — per-hour API credits are shared org-wide and easy to exhaust.
- Use COQL for server-side filtered queries rather than fetching all records and filtering client-side — COQL reduces API calls and keeps list sizes manageable on mobile screens.
- Check for status 201 (not 200) after POST create calls — Zoho CRM returns 201 Created for successful record creation, so conditional success checks must test for 201.
- Scope your self-client OAuth grant to only the modules you need (e.g. `ZohoCRM.modules.leads.ALL`) rather than requesting all CRM permissions — this limits exposure if the refresh token is ever compromised.
Alternatives
Choose HubSpot if you want a more developer-friendly API with straightforward Bearer token auth and a generous free tier — HubSpot's v3 API avoids the region and header quirks of Zoho CRM.
Choose Salesforce if your organization needs enterprise CRM features with SOQL query power and a larger ecosystem of third-party integrations — though Salesforce's API setup is similarly complex.
Choose Zoho Books if your primary need is invoicing and accounting rather than sales pipeline management — Books uses the same Zoho OAuth model but targets financial records with a mandatory organization_id parameter.
Frequently asked questions
Does FlutterFlow have a native Zoho CRM connector?
No. FlutterFlow does not have a built-in Zoho CRM integration. You connect using the API Calls panel to configure the Zoho CRM v7 REST endpoints manually. Because Zoho's OAuth refresh token is a server-side credential, you also need a Firebase Cloud Function to handle token refresh securely.
Can I use the same Zoho self-client for Zoho CRM and Zoho Books?
Yes. A single self-client in the Zoho API Console can be granted scopes for multiple Zoho products. Include scopes for both services when generating the grant code: for example, `ZohoCRM.modules.leads.ALL,ZohoBooks.invoices.READ`. The refresh token returned covers all granted scopes. However, keep separate Firebase Cloud Functions or separate endpoint configs for each service to keep the code clean.
What's the difference between Zoho CRM's v2 and v7 APIs?
Zoho CRM v7 is the current stable version and adds features like COQL (SQL-style queries), bulk API support, and improved pagination. The base URL changes from `/crm/v2` to `/crm/v7`. New integrations should always target v7. If you have existing code targeting v2, it continues to work but Zoho may deprecate older versions in the future — check the Zoho CRM API changelog for the current support timeline.
How do I handle Zoho CRM's API rate limits in FlutterFlow?
Zoho CRM allocates API call credits per hour on an org-wide basis — approximately 3,000 calls/hour on lower tiers. In FlutterFlow, avoid auto-refreshing ListViews more than once every 30–60 seconds. Cache list results in App State and re-fetch only when the user explicitly pulls to refresh or navigates to a new record. For filtered searches, use COQL to fetch only needed records rather than fetching all records and filtering on-device.
Can I use Zoho CRM with a FlutterFlow web app as well as mobile?
Yes, but web builds are subject to CORS. If your Zoho CRM API calls originate from a browser (FlutterFlow web), the browser may block the cross-origin request. The Firebase Cloud Function proxy already handles CORS for the token endpoint. If you route all CRM calls through the Cloud Function (not just token refresh), CORS issues are avoided for web builds as well. Mobile builds are not subject to CORS.
What FlutterFlow plan do I need for this integration?
You need at least FlutterFlow's Starter plan to access the API Calls panel and App State, which are required for this integration. Firebase Cloud Functions require the Firebase Blaze (pay-as-you-go) plan, though actual function invocation costs are minimal for typical CRM usage volumes.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation