Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

Zoho CRM

Connect Bubble to Zoho CRM using OAuth 2.0 self-client credentials — but watch for two Bubble-specific traps: the authorization header must be `Zoho-oauthtoken [token]` (not Bearer), and Zoho operates five regional data centers with separate API domains. A Backend Workflow handles the refresh token exchange server-side so your client_secret never reaches the browser, keeping credentials secure in Bubble's Private headers.

What you'll learn

  • How to generate a Zoho CRM self-client OAuth 2.0 refresh token without a redirect URI
  • How to identify your Zoho data center and set the correct regional API base URL
  • Why the Authorization header must use `Zoho-oauthtoken` prefix instead of Bearer
  • How to build a Bubble Backend Workflow that exchanges the refresh token for an access token and stores it server-side
  • How to use COQL (Zoho's query language) in a Bubble API Connector call for efficient CRM searches
  • How to structure write operations with Zoho's required `data` array wrapper for POST/PUT calls
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read3–4 hoursCRM & SalesLast updated July 2026RapidDev Engineering Team
TL;DR

Connect Bubble to Zoho CRM using OAuth 2.0 self-client credentials — but watch for two Bubble-specific traps: the authorization header must be `Zoho-oauthtoken [token]` (not Bearer), and Zoho operates five regional data centers with separate API domains. A Backend Workflow handles the refresh token exchange server-side so your client_secret never reaches the browser, keeping credentials secure in Bubble's Private headers.

Quick facts about this guide
FactValue
ToolZoho CRM
CategoryCRM & Sales
MethodBubble API Connector
DifficultyIntermediate
Time required3–4 hours
Last updatedJuly 2026

Two Bubble-specific traps that break Zoho CRM integrations

Most Zoho CRM tutorials show a generic `Authorization: Bearer [token]` pattern — and that is exactly what breaks in Bubble. Zoho CRM requires the header value to start with `Zoho-oauthtoken`, not `Bearer`. If you select Bubble's built-in 'OAuth 2.0' authentication type, Bubble automatically prepends 'Bearer' and your API calls return 401 errors silently — no indication the prefix is wrong.

The second trap is Zoho's regional data center architecture. Zoho operates five separate API domains: `zohoapis.com` (US), `zohoapis.eu` (Europe), `zohoapis.in` (India), `zohoapis.com.au` (Australia), and `zohoapis.jp` (Japan). If your Zoho account lives in the EU region but your Bubble API Connector points to the US domain, Zoho returns `INVALID_TOKEN` — even though the token itself is perfectly valid. Check which region you log into (zoho.eu vs zoho.com) and match the API base URL accordingly.

The self-client OAuth flow is well-suited for Bubble because there is no browser redirect involved — you generate the grant code manually in Zoho's API console, exchange it for a refresh token once, and then Bubble's Backend Workflow handles all future token refreshes automatically.

Integration method

Bubble API Connector

API Connector with a custom `Zoho-oauthtoken` Authorization header (Private) plus a Backend Workflow that handles OAuth 2.0 refresh token exchange to Zoho's token endpoint.

Prerequisites

  • A Bubble app on Starter plan or above — Backend Workflows required for OAuth token refresh are not available on Bubble Free
  • A Zoho CRM account with admin or API access permissions
  • The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
  • Access to Zoho's API console at api-console.zoho.com to create a self-client application

Step-by-step guide

1

Create a Zoho self-client app and generate your refresh token

Open api-console.zoho.com in your browser and sign in with your Zoho account. Click 'Add Client' and select 'Self Client' from the application type options — this type is specifically designed for server-to-server integrations without a redirect URI, which is exactly what Bubble needs. Give your client a name like 'Bubble Integration' and click 'Create Now.' You will immediately see your Client ID and Client Secret — copy both and store them securely. Next, generate a grant code. Click the 'Generate Code' tab. In the Scope field, enter the permissions your Bubble app needs. For standard CRM access use: `ZohoCRM.modules.ALL,ZohoCRM.settings.ALL,ZohoCRM.coql.READ`. Set the Time Duration to the maximum available (typically 10 minutes) and click 'Create.' Copy the generated grant code — it expires in the time you selected, so act quickly. Now exchange the grant code for a refresh token. Open your browser's address bar and construct this URL — do NOT use Bubble for this step, it must be done manually once: ``` https://accounts.zoho.com/oauth/v2/token?code=YOUR_GRANT_CODE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https://www.zoho.com&grant_type=authorization_code ``` Replace the placeholders and paste this URL into your browser. You will receive a JSON response containing `refresh_token` — copy it. This refresh token does not expire unless unused for 60 days or manually revoked. Store it safely alongside your Client ID and Secret.

zoho_oauth_setup.txt
1// Required OAuth scopes for full CRM access:
2// ZohoCRM.modules.ALL → read/write all CRM modules (Leads, Contacts, Deals, Accounts)
3// ZohoCRM.settings.ALL → read CRM settings and field metadata
4// ZohoCRM.coql.READ → run COQL queries via POST /coql endpoint
5
6// Token exchange URL (run ONCE in browser address bar):
7// https://accounts.zoho.com/oauth/v2/token
8// ?code=<grant_code>
9// &client_id=<client_id>
10// &client_secret=<client_secret>
11// &redirect_uri=https://www.zoho.com
12// &grant_type=authorization_code
13
14// Response will contain:
15// { "access_token": "...", "refresh_token": "...", "token_type": "Bearer", "expires_in": 3600 }

Pro tip: The `redirect_uri=https://www.zoho.com` is a placeholder — Zoho's self-client flow does not actually redirect anywhere. This value just needs to be a valid URL to satisfy the OAuth endpoint. You are running this exchange manually in the browser, not via a redirect flow.

Expected result: You have a Client ID, Client Secret, and a long-lived refresh_token stored securely. You also know your Zoho data center region (check which URL you log into — zoho.com, zoho.eu, zoho.in, etc.).

2

Create the Zoho_Auth data type and Backend Workflow for token refresh

Before configuring the API Connector, set up the data structure and Backend Workflow that will keep your access token fresh. In your Bubble editor, go to the Data tab and click 'Add a data type.' Name it `Zoho_Auth`. Add three fields: `access_token` (text), `expires_at` (date), and `region` (text). This data type will hold exactly one record — the current valid access token. Now apply a privacy rule. Click 'Privacy' in the Data tab, click the Zoho_Auth data type, and add a rule: 'Everyone else → no fields visible.' This prevents end users from reading your access tokens through Bubble's Data API. Check 'Ignore privacy rules when using workflows' so Backend Workflows can still read the token. Now go to Backend Workflows (left sidebar). Click 'Add workflow' and name it `Refresh Zoho Token`. Inside this workflow, add the following actions: 1. Add action: 'API Connector → Refresh Access Token' (you will create this call in the next step — for now, plan for it). The call POSTs to Zoho's token endpoint with your refresh token. 2. Add action: Search for Zoho_Auth first item. If found, update `access_token` to the result and `expires_at` to Current date/time + 3540 seconds (59 minutes — slightly under the 3600-second TTL for safety). If not found, create a new Zoho_Auth record with the same values. You will link step 1 of this workflow to the actual API Connector call in the next step.

zoho_auth_datatype.txt
1// Zoho_Auth data type:
2// access_token → text
3// expires_at → date
4// region → text (e.g., 'com', 'eu', 'in', 'com.au')
5
6// Privacy rule on Zoho_Auth:
7// Everyone else → no fields visible
8// ✓ Ignore privacy rules when using workflows
9
10// Backend Workflow: Refresh Zoho Token
11// Step 1: API Connector → Zoho → Refresh Access Token (POST)
12// Step 2: Search for Zoho_Auth's first item
13// IF found: Make changes → access_token = Step1's access_token
14// expires_at = Current date/time + 3540 seconds
15// IF not found: Create new Zoho_Auth
16// access_token = Step1's access_token
17// expires_at = Current date/time + 3540 seconds

Pro tip: Using 3540 seconds (59 minutes) instead of 3600 for expires_at creates a 60-second safety buffer. Tokens checked at exactly 3600 seconds can expire during the API call window, causing intermittent failures.

Expected result: Zoho_Auth data type exists with access_token, expires_at, and region fields. A privacy rule blocks end users from reading it. The Refresh Zoho Token Backend Workflow structure is ready to be wired to API Connector calls.

3

Configure the Zoho CRM API Connector with token refresh and COQL search

In your Bubble editor, go to Plugins → API Connector. Click 'Add another API' and name it `Zoho CRM`. Set the base URL based on your data center: - US accounts: `https://www.zohoapis.com/crm/v7` - EU accounts: `https://www.zohoapis.eu/crm/v7` - India accounts: `https://www.zohoapis.in/crm/v7` - Australia accounts: `https://www.zohoapis.com.au/crm/v7` Add a shared header (applies to all calls in this group): Header name `Authorization`, value `Zoho-oauthtoken <access_token>`. IMPORTANT: the prefix is `Zoho-oauthtoken` with a space before the token — not `Bearer`. Mark this header as a dynamic parameter of type Private so your Backend Workflow can inject the current token at runtime. Now add two API Connector calls within this group: Call 1 — Refresh Access Token: Click 'Add another call.' Name it `Refresh Access Token`. Method: POST. URL: `https://accounts.zoho.com/oauth/v2/token` (use your region's account URL if outside US — e.g., accounts.zoho.eu/oauth/v2/token). In the Body section, add these parameters: `grant_type` = `refresh_token`, `client_id` = your Client ID (mark Private), `client_secret` = your Client Secret (mark Private), `refresh_token` = your refresh token (mark Private). Set 'Use as' to Action. Click Initialize call — it should return a JSON response with `access_token`. Call 2 — COQL Search: Click 'Add another call.' Name it `COQL Search`. Method: POST. URL: `/coql` (relative to base URL). JSON body: `{ "select_query": "<query>" }` where `<query>` is a dynamic parameter. Set 'Use as' to Data. Initialize call with a test query: `SELECT Id, First_Name, Last_Name, Email, Phone FROM Lead LIMIT 5`. Now go back to your Refresh Zoho Token Backend Workflow and wire step 1 to the 'Refresh Access Token' call you just created.

zoho_api_connector_config.json
1// Zoho CRM API Connector — shared header configuration
2{
3 "base_url": "https://www.zohoapis.com/crm/v7", // change .com to .eu / .in / .com.au for other regions
4 "shared_headers": {
5 "Authorization": "Zoho-oauthtoken <access_token>" // mark as Private dynamic param
6 }
7}
8
9// Call 1: Refresh Access Token
10{
11 "method": "POST",
12 "url": "https://accounts.zoho.com/oauth/v2/token",
13 "body": {
14 "grant_type": "refresh_token",
15 "client_id": "<your_client_id>", // mark Private
16 "client_secret": "<your_client_secret>", // mark Private
17 "refresh_token": "<your_refresh_token>" // mark Private
18 }
19}
20
21// Call 2: COQL Search
22{
23 "method": "POST",
24 "url": "https://www.zohoapis.com/crm/v7/coql",
25 "headers": { "Authorization": "Zoho-oauthtoken <access_token>" },
26 "body": {
27 "select_query": "<dynamic_coql_query>"
28 }
29}
30
31// Example COQL queries:
32// Leads owned by a specific user:
33// SELECT Id, First_Name, Last_Name, Email, Stage FROM Lead WHERE Owner = 'user@example.com' LIMIT 20
34//
35// Open deals by closing date:
36// SELECT Id, Deal_Name, Amount, Stage, Closing_Date FROM Deal WHERE Stage != 'Closed Won' ORDER BY Closing_Date ASC LIMIT 10

Pro tip: COQL module names are singular — use `FROM Lead` not `FROM Leads`, `FROM Deal` not `FROM Deals`. Using the plural form returns a parse error. Also note: COQL returns HTTP 201 for successful POST queries (not 200), which can confuse Bubble workflow success checks.

Expected result: The Refresh Access Token call successfully returns a JSON response with `access_token` on initialization. The COQL Search call returns lead records. Both calls show in the API Connector's call list and can be referenced in Bubble workflows.

4

Add write operations for creating and updating CRM records

Zoho CRM's write operations (create and update) require wrapping all record data inside a `data` array — flat JSON returns a 400 error. In the API Connector, within your Zoho CRM group, add two more calls: Call 3 — Create Lead: Method POST, URL `/Leads`. JSON body: ```json { "data": [ { "First_Name": "<first_name>", "Last_Name": "<last_name>", "Email": "<email>", "Phone": "<phone>", "Lead_Source": "<lead_source>" } ] } ``` Mark each `<dynamic>` field as a parameter that can be passed from the Bubble workflow. Set 'Use as' to Action. Call 4 — Update Deal Stage: Method PUT, URL `/Deals/<deal_id>` (mark `deal_id` as a dynamic parameter). JSON body: ```json { "data": [ { "Stage": "<stage>" } ] } ``` Now build the frontend workflows. When a user submits a form, trigger a Backend Workflow that: 1. Checks if Zoho_Auth's first item's expires_at is before Current date/time (or if no record exists) 2. If stale/missing: calls the Refresh Zoho Token workflow 3. Then calls the Create Lead action with the current access_token from Zoho_Auth and the form field values For the COQL search, bind a Repeating Group's data source to 'Get data from an external API → Zoho CRM → COQL Search' and pass the query string as a parameter built from page inputs. Always inject the current access_token from Zoho_Auth into the Authorization header parameter before the call.

zoho_write_calls.json
1// Call 3: Create Lead
2{
3 "method": "POST",
4 "url": "https://www.zohoapis.com/crm/v7/Leads",
5 "headers": {
6 "Authorization": "Zoho-oauthtoken <access_token>", // Private
7 "Content-Type": "application/json"
8 },
9 "body": {
10 "data": [
11 {
12 "First_Name": "<first_name>",
13 "Last_Name": "<last_name>",
14 "Email": "<email>",
15 "Phone": "<phone>",
16 "Lead_Source": "<lead_source>"
17 }
18 ]
19 }
20}
21
22// Call 4: Update Deal Stage
23{
24 "method": "PUT",
25 "url": "https://www.zohoapis.com/crm/v7/Deals/<deal_id>",
26 "headers": {
27 "Authorization": "Zoho-oauthtoken <access_token>", // Private
28 "Content-Type": "application/json"
29 },
30 "body": {
31 "data": [
32 {
33 "Stage": "<stage>"
34 }
35 ]
36 }
37}

Pro tip: When sending an update (PUT), Zoho CRM returns HTTP 200 with a `data` array containing the updated record's ID and a `details` block. Bubble's API Connector can parse this — initialize the call with a valid deal ID to auto-detect the response shape.

Expected result: Create Lead and Update Deal Stage calls are configured in the API Connector. A test lead creation returns a successful response with the new record's ID. Deal stage updates reflect immediately in your Zoho CRM account.

5

Handle token refresh and display CRM data in the Bubble UI

The final piece is wiring token refresh logic into every workflow that calls Zoho CRM. Bubble does not automatically retry failed calls — you need an explicit check before each API action. In any workflow that makes a Zoho CRM call: 1. Add a first action with a condition: 'Only when Zoho_Auth's count = 0 OR Zoho_Auth's first item's expires_at < Current date/time.' 2. Inside that condition: add action 'Schedule API Workflow → Refresh Zoho Token' and set it to run at Current date/time (immediate). 3. Then add the actual Zoho API call, referencing `Do a search for Zoho_Auth's first item's access_token` in the Authorization header parameter. For displaying CRM data in a Repeating Group: set the data source to 'Get data from an external API,' select 'Zoho CRM - COQL Search,' and build the COQL query string dynamically from dropdown and text input values on the page. Bubble's text composition handles string building for COQL. For example: `'SELECT Id, First_Name, Last_Name, Email FROM Lead WHERE Stage = '' + Dropdown_Stage's value + '' LIMIT 20'`. For a richer search experience, add a search input and build a COQL WHERE clause: `Email LIKE '%' + SearchInput's value + '%' OR First_Name LIKE '%' + SearchInput's value + '%'`. COQL's LIKE operator uses `%` wildcards. RapidDev's team has built dozens of Bubble apps with Zoho CRM and similar CRM integrations — if you would like a free scoping call on your specific data model and workflow design, visit rapidevelopers.com/contact.

zoho_token_refresh_pattern.txt
1// Token freshness check pattern (add before every Zoho API action):
2// Condition: Only when:
3// (Do a search for Zoho_Auth:count is 0)
4// OR (Do a search for Zoho_Auth's first item's expires_at < Current date/time)
5// Action inside condition: Schedule API Workflow → Refresh Zoho Token → at Current date/time
6
7// Then in the main API call action:
8// Authorization param = Do a search for Zoho_Auth's first item's access_token
9
10// COQL query for filtered lead search:
11// SELECT Id, First_Name, Last_Name, Email, Phone, Stage, Lead_Source
12// FROM Lead
13// WHERE Stage = '<selected_stage>'
14// AND (First_Name LIKE '%<search_term>%' OR Last_Name LIKE '%<search_term>%')
15// ORDER BY Created_Time DESC
16// LIMIT 20

Pro tip: COQL queries sent to the /coql endpoint return a 201 status code, not 200. If you have conditional logic checking for HTTP 200 as a success indicator, it will incorrectly treat a successful COQL response as a failure. Bubble's API Connector handles 201 responses correctly — just make sure any custom error-checking workflows use 'is not empty' checks on the response data rather than status code comparison.

Expected result: Your Bubble app reads Zoho CRM leads and deals in a Repeating Group, creates new leads from a form, and updates deal stages via button clicks — all with automatic token refresh that keeps the integration running without manual intervention.

Common use cases

Custom CRM portal for field sales reps

Build a mobile-friendly Bubble app that gives field sales representatives a simplified view of their assigned leads and deals — without needing full Zoho CRM access. The app reads lead status, contact information, and deal stage from Zoho via COQL queries, and lets reps update deal stages with a button tap.

Bubble Prompt

Show a Repeating Group of Leads filtered by Owner = Current User, pulling data from the Zoho CRM API Connector's COQL search call. Each row shows the lead name, company, phone number, and a 'Move to Qualified' button that triggers a PATCH call to update the lead's stage.

Copy this prompt to try it in Bubble

Lead capture form that syncs to Zoho CRM

Replace Zoho's native web forms with a custom-branded Bubble form that validates inputs, shows inline errors, and posts new lead records directly to Zoho CRM — all from a Bubble page workflow. The POST call wraps the lead data in Zoho's required `data` array structure.

Bubble Prompt

When a user submits the contact form, run a Backend Workflow that POSTs to Zoho CRM /crm/v7/Leads with a body of { data: [{ First_Name, Last_Name, Email, Phone, Lead_Source: 'Web Form' }] }, then show a 'Thank you' message and send a confirmation email via SendGrid.

Copy this prompt to try it in Bubble

Deal pipeline dashboard with COQL search

Display a live sales pipeline dashboard in Bubble using COQL queries to pull deals by stage. Instead of fetching all 200 deals and filtering client-side (which burns Zoho API calls and Bubble WU), the COQL query returns only the records matching the selected pipeline stage.

Bubble Prompt

On page load, run a COQL query: SELECT Id, Deal_Name, Amount, Stage, Closing_Date FROM Deal WHERE Stage = 'Proposal/Price Quote' ORDER BY Closing_Date ASC LIMIT 20. Display results in a Repeating Group sorted by closing date.

Copy this prompt to try it in Bubble

Troubleshooting

API calls return 401 with `INVALID_CLIENT` or blank authorization error despite correct credentials

Cause: The Authorization header prefix is wrong. Bubble's built-in 'OAuth 2.0' authentication type adds 'Bearer' automatically, but Zoho CRM requires `Zoho-oauthtoken` as the prefix. Any prefix other than `Zoho-oauthtoken` (including Bearer, Zoho-oauth-token with a hyphen, or no prefix) returns 401 silently without a helpful error message.

Solution: In API Connector → Zoho CRM → shared headers, add the Authorization header manually (do not use Bubble's built-in OAuth type). Set the value to exactly `Zoho-oauthtoken ` followed by your token parameter — note the space after `Zoho-oauthtoken`. Verify the header value in the API Connector UI shows `Zoho-oauthtoken <your_param_name>`, not `Bearer <your_param_name>`.

typescript
1// Correct header format in API Connector:
2// Header name: Authorization
3// Header value: Zoho-oauthtoken <access_token> ← note: no 'Bearer' prefix
4
5// Wrong (causes silent 401):
6// Authorization: Bearer <access_token>
7// Authorization: Zoho-oauth-token <access_token> ← wrong hyphenation
8// Authorization: Zoho-oauthtoken<access_token> ← missing space

API returns `INVALID_TOKEN` even though the token was recently generated

Cause: Your Zoho account is on a non-US data center, but the API Connector base URL is pointing to `zohoapis.com` (US). Each Zoho data center has its own separate API domain, and tokens from one region are not valid on another region's endpoint.

Solution: Log into your Zoho account and check the URL in your browser's address bar. If it shows `crm.zoho.eu`, your account is on the EU data center — change the API Connector base URL to `https://www.zohoapis.eu/crm/v7`. Similarly, India accounts use `zohoapis.in`, Australia accounts use `zohoapis.com.au`. Also update the token endpoint in the Refresh Access Token call: `accounts.zoho.eu/oauth/v2/token` for EU accounts.

typescript
1// Regional base URLs:
2// US: https://www.zohoapis.com/crm/v7 → accounts.zoho.com/oauth/v2/token
3// EU: https://www.zohoapis.eu/crm/v7 → accounts.zoho.eu/oauth/v2/token
4// India: https://www.zohoapis.in/crm/v7 → accounts.zoho.in/oauth/v2/token
5// Australia: https://www.zohoapis.com.au/crm/v7 → accounts.zoho.com.au/oauth/v2/token

'There was an issue setting up your call' error when initializing the COQL Search call

Cause: The Initialize call requires a real successful response from Zoho's API to auto-detect the response schema. If the access token is expired or the COQL query syntax is invalid, Zoho returns an error — which Bubble interprets as a setup failure rather than a data error.

Solution: First ensure your Refresh Access Token call initializes successfully and returns a valid access_token. Then use that token value as a hardcoded temporary value in the Authorization header for the COQL call initialization. Use a simple test query: `SELECT Id, First_Name FROM Lead LIMIT 3`. Once initialization succeeds, switch the Authorization header back to the dynamic parameter referencing Zoho_Auth.

POST to /Leads returns 400 with 'mandatory field missing' or 'invalid data format'

Cause: Zoho CRM write operations require all record fields to be wrapped inside a `data` array. Sending a flat JSON object like `{ "First_Name": "...", "Last_Name": "..." }` returns a 400 error. Additionally, `Last_Name` is a mandatory field for Lead creation.

Solution: Wrap all record fields in the `data` array in the API Connector body: `{ "data": [ { "First_Name": "...", "Last_Name": "..." } ] }`. Ensure `Last_Name` is always provided and non-empty — it is a required field. Use Bubble's workflow conditions to validate form inputs before triggering the API call.

typescript
1// Correct POST body format:
2{
3 "data": [
4 {
5 "First_Name": "<first_name>",
6 "Last_Name": "<last_name>", // mandatory
7 "Email": "<email>",
8 "Lead_Source": "<lead_source>"
9 }
10 ]
11}

Best practices

  • Always store Zoho access tokens in a Bubble data type with an expires_at timestamp — never hardcode the token in the API Connector or request a new token on every call. Token requests count toward your Zoho API quota.
  • Mark the Authorization header as a Private dynamic parameter in the API Connector. Without the Private checkbox, the token appears in browser DevTools network requests where any user can read it.
  • Apply Bubble privacy rules to your Zoho_Auth data type — set 'Everyone else → no fields visible' and enable 'Ignore privacy rules when using workflows' for Backend Workflows that read tokens.
  • Use COQL queries (POST /coql) for filtered CRM searches instead of fetching large record sets and filtering client-side. COQL runs the filter on Zoho's servers and returns only the matching records, saving both Zoho API credits and Bubble WU.
  • Add a proactive token refresh cron job for apps with dormant users. Zoho refresh tokens expire after 60 days of inactivity — if no one logs in for two months, the integration breaks silently. Schedule a weekly Backend Workflow that refreshes the access token to keep the refresh token alive.
  • Verify your Zoho data center before building. Use the wrong regional API domain (e.g., zohoapis.com for an EU account) and you will spend hours debugging valid credentials that appear to fail.
  • Test all write operations (POST/PUT) with the exact `data` array structure before building frontend workflows. A missing `data` wrapper returns 400 errors that are easy to misdiagnose as auth problems.
  • Backend Workflows for the token refresh logic require a Bubble Starter plan or above — plan this into your project budget before starting the integration.

Alternatives

Frequently asked questions

Can I use Zoho CRM with Bubble's Free plan?

Partially. You can configure the API Connector and make read calls from client-side workflows on the Free plan. However, the OAuth refresh token exchange (which keeps the integration working long-term) requires a Backend Workflow — and Backend Workflows are only available on Bubble Starter plan and above. Without this, you would need to manually update the access token in your API Connector every hour.

Why can't I use Bubble's built-in OAuth 2.0 authentication type for Zoho?

Bubble's OAuth 2.0 type automatically adds `Bearer` as the authorization prefix, but Zoho CRM requires `Zoho-oauthtoken` as the prefix. These look similar but Zoho's API rejects Bearer tokens silently with a 401. The correct approach is to add the Authorization header manually as a custom header and build the `Zoho-oauthtoken [token]` string yourself with a Private parameter.

What happens when my refresh token expires after 60 days?

If the refresh token expires (due to 60 days of inactivity), you need to go back to api-console.zoho.com, generate a new grant code, exchange it for a new refresh token, and update the Private parameter in your Bubble API Connector. This is a manual process. Prevent it by scheduling a weekly Backend Workflow that calls the refresh endpoint — this keeps the refresh token active.

Why does my COQL search return HTTP 201 instead of 200?

Zoho's COQL endpoint intentionally returns HTTP 201 for successful POST queries — this is documented behavior. It does not mean the query failed. Bubble's API Connector handles 201 responses correctly when reading the response body. Avoid building workflow conditions that check for status 200 as a success indicator for COQL calls.

Can I connect to multiple Zoho CRM accounts from one Bubble app?

Yes, but it requires separate API Connector API groups per account, each with their own Authorization header parameters, and separate Zoho_Auth records per account (add an `account_name` field to distinguish them). For apps serving multiple clients, consider building a multi-tenant token store with a field linking each Zoho_Auth record to the relevant workspace or client record in your Bubble database.

What is the Zoho CRM API rate limit?

Zoho CRM rate limits vary by plan. Professional plan accounts typically allow around 3,000 API calls per hour, while Enterprise accounts allow more. Check the current limits at zoho.com/crm/pricing, as these figures change. COQL queries count as one API call per request. Minimize calls by using COQL's LIMIT clause and caching results in Bubble's database for data that does not change frequently.

RapidDev

Talk to an Expert

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

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

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.