Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsFlutterFlow API Call

Zoho Books

Connect FlutterFlow to Zoho Books v3 using the REST API with region-specific base URLs and OAuth tokens refreshed by a Firebase Cloud Function. Every Zoho Books API call requires an `organization_id` query parameter — fetch it once from `/organizations`, store it in App State, and append it to all invoice and customer requests. The `Zoho-oauthtoken` auth header prefix is mandatory.

What you'll learn

  • How to configure a Zoho self-client OAuth app for Zoho Books and generate a refresh token
  • How to deploy a Firebase Cloud Function that exchanges the Zoho refresh token for a 1-hour access token
  • Why the Zoho Books `organization_id` parameter is mandatory on every API call and how to fetch it once at startup
  • How to create a Zoho Books v3 API Group in FlutterFlow with region-specific base URLs and correct auth header
  • How to build a mobile invoice viewer with `$.invoices` JSON Path and manage the low daily API call limits
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced16 min read60 minutesCRM & SalesLast updated July 2026RapidDev Engineering Team
TL;DR

Connect FlutterFlow to Zoho Books v3 using the REST API with region-specific base URLs and OAuth tokens refreshed by a Firebase Cloud Function. Every Zoho Books API call requires an `organization_id` query parameter — fetch it once from `/organizations`, store it in App State, and append it to all invoice and customer requests. The `Zoho-oauthtoken` auth header prefix is mandatory.

Quick facts about this guide
FactValue
ToolZoho Books
CategoryCRM & Sales
MethodFlutterFlow API Call
DifficultyAdvanced
Time required60 minutes
Last updatedJuly 2026

Zoho Books in FlutterFlow: the `organization_id` You Must Never Forget

Zoho Books v3 is Zoho's accounting and invoicing REST API, and it shares the same OAuth self-client mechanism as Zoho CRM. The auth plumbing is nearly identical: register a self-client in the Zoho API Console, generate a refresh token scoped to ZohoBooks, deploy a Firebase Cloud Function that exchanges it for 1-hour access tokens, and use the `Authorization: Zoho-oauthtoken [token]` header (the non-standard Zoho prefix that differs from Bearer). However, Zoho Books adds one additional mandatory parameter that will break your integration if you forget it: `organization_id`.

Every Zoho Books API call — reading invoices, listing customers, fetching payments — requires `?organization_id={id}` as a query parameter on every request. Without it, the API returns HTTP 400 regardless of how correct your authentication is. The organization ID is specific to your Zoho Books organization and is obtained by calling `GET /organizations` on app startup — this bootstrap endpoint does not require the org ID. Extract `$.organizations[0].organization_id`, store it in App State, and append it to all subsequent calls.

Zoho Books offers a free tier for small businesses (revenue-capped; verify terms at zoho.com/books/pricing). Paid plans start around $15 per organization per month on the Standard tier (verify current pricing). API limits are 2,500 calls per day on the free plan and 5,000 per day on paid plans — much tighter than Zoho CRM's hourly credits. Cache invoice and customer lists in App State and re-fetch only on explicit user pull-to-refresh to stay within these daily limits.

Integration method

FlutterFlow API Call

FlutterFlow queries Zoho Books v3 REST endpoints using API Calls with the `Authorization: Zoho-oauthtoken [token]` header and a mandatory `?organization_id=` query parameter on every request. The OAuth refresh token is a server secret stored in a Firebase Cloud Function that returns short-lived 1-hour access tokens. FlutterFlow fetches the organization ID once at startup, stores both the token and org ID in App State, and appends the org ID to all subsequent API calls.

Prerequisites

  • A Zoho Books account — free tier (revenue-capped) or a paid plan, with at least one organization set up
  • Access to the Zoho API Console at api-console.zoho.com to register a self-client OAuth app
  • Knowledge of which Zoho data center your account uses (US, EU, India, or Australia)
  • 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

Step-by-step guide

1

Register a Zoho self-client and generate a Books-scoped refresh token

Go to api-console.zoho.com and sign in with your Zoho account. Click '+ Add Client' → select 'Self Client'. Once the self-client is created, copy the Client ID and Client Secret — these go into your Firebase Cloud Function, never into FlutterFlow. Click the 'Generate Code' tab. In the Scope field, enter scopes for Zoho Books specifically: `ZohoBooks.invoices.READ,ZohoBooks.invoices.CREATE,ZohoBooks.customers.READ,ZohoBooks.settings.READ`. The `ZohoBooks.settings.READ` scope is required to call the `/organizations` endpoint. Set the Time Duration to the maximum available (typically 10 minutes) and click 'Create'. Copy the generated grant code immediately — it expires quickly. Exchange this grant code for a refresh token by making a POST to the Zoho token endpoint. For US accounts: `https://accounts.zoho.com/oauth/v2/token`; for EU: `https://accounts.zoho.eu/oauth/v2/token`; for India: `https://accounts.zoho.in/oauth/v2/token`; for Australia: `https://accounts.zoho.com.au/oauth/v2/token`. Post form-encoded body params: `grant_type=authorization_code`, `client_id=[your client id]`, `client_secret=[your client secret]`, `code=[your grant code]`, and `redirect_uri=` (empty). The response contains a `refresh_token` — copy it. This is the long-lived credential that goes into Firebase. Note: if you already have a Zoho self-client from a Zoho CRM integration, you can reuse it — just re-generate a grant code with the ZohoBooks scopes instead of ZohoCRM scopes to get a Books-scoped refresh token.

token_exchange.txt
1// Exchange grant code for refresh token (run once, e.g. in browser console or Postman)
2POST https://accounts.zoho.com/oauth/v2/token
3Content-Type: application/x-www-form-urlencoded
4
5grant_type=authorization_code
6&client_id=YOUR_CLIENT_ID
7&client_secret=YOUR_CLIENT_SECRET
8&code=YOUR_GRANT_CODE
9&redirect_uri=
10

Pro tip: Use the `ZohoBooks.settings.READ` scope (in addition to invoices/customers) when generating the grant code — this scope is required to call `GET /organizations` to retrieve the mandatory organization_id. Without it, the bootstrap call will fail with a 403.

Expected result: The token exchange response contains a `refresh_token` field. Store it securely — it is the root credential for all Zoho Books access from your FlutterFlow app.

2

Deploy a Firebase Cloud Function for Zoho Books token refresh

Open your Firebase project → Build → Functions. The Cloud Function stores your Zoho Client ID, Client Secret, and Refresh Token, and returns a fresh access token to FlutterFlow whenever called. Create the function code shown below. In your Firebase CLI, configure the function environment: `firebase functions:config:set zohobooks.client_id="..." zohobooks.client_secret="..." zohobooks.refresh_token="..." zohobooks.region="com"` — use `eu`, `in`, or `com.au` for the region if your account is not US-based. Deploy with `firebase deploy --only functions` and copy the HTTPS trigger URL. The function is nearly identical to the Zoho CRM token function — the only difference is the environment key names. If you have both integrations, you can consolidate them into a single multi-service token function or keep them separate for clarity. CORS headers are included so FlutterFlow web builds can call the function from a browser.

index.js
1const functions = require('firebase-functions');
2const axios = require('axios');
3
4exports.getZohoBooksToken = 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 }
11
12 const config = functions.config().zohobooks;
13 const tokenUrl = `https://accounts.zoho.${config.region}/oauth/v2/token`;
14
15 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');
21
22 const response = await axios.post(tokenUrl, params);
23 return res.json({
24 access_token: response.data.access_token,
25 expires_in: response.data.expires_in
26 });
27 } catch (err) {
28 console.error('Zoho Books token error:', err.response?.data || err.message);
29 return res.status(500).json({ error: 'Token refresh failed' });
30 }
31});
32

Pro tip: The Zoho region in the token URL must match your Zoho account's data center exactly. Use `accounts.zoho.com` for US, `accounts.zoho.eu` for EU, `accounts.zoho.in` for India, and `accounts.zoho.com.au` for Australia. A mismatch returns 401 regardless of how correct the credentials are.

Expected result: Calling the Cloud Function URL in a browser returns `{"access_token": "...", "expires_in": 3600}`. The token is valid for 1 hour.

3

Create the Zoho Books v3 API Group in FlutterFlow

Open FlutterFlow → left nav → API Calls → '+ Add' → 'Create API Group'. Name it 'ZohoBooks'. Set the Base URL to your region's Zoho Books API endpoint: - US: `https://www.zohoapis.com/books/v3` - EU: `https://www.zohoapis.eu/books/v3` - India: `https://www.zohoapis.in/books/v3` - Australia: `https://www.zohoapis.com.au/books/v3` Add shared headers at the group level: - `Authorization`: `Zoho-oauthtoken [accessToken]` — add a Variable named `accessToken` of type String and use it here. The prefix is `Zoho-oauthtoken`, not `Bearer`. - `Content-Type`: `application/json` Now add individual API Calls inside the ZohoBooks group: 1. 'getOrganizations' — Method GET, endpoint `/organizations`. This is the bootstrap call. It does NOT need `organization_id` — it returns the list of your organizations. Parse `$.organizations` and specifically `$.organizations[0].organization_id`. 2. 'getInvoices' — Method GET, endpoint `/invoices?organization_id=[organizationId]&sort_column=date&sort_order=D&per_page=50`. Add a Variable `organizationId` of type String. Parse `$.invoices`. 3. 'getCustomers' — Method GET, endpoint `/contacts?organization_id=[organizationId]&contact_type=customer&per_page=100`. Parse `$.contacts`. 4. 'createInvoice' — Method POST, endpoint `/invoices?organization_id=[organizationId]`. Body as JSON with variables for `customerId`, `lineItemName`, `lineItemPrice`, `lineItemQuantity`. In the Response & Test tab for `getInvoices`, paste a sample Zoho Books invoice list response and click 'Generate JSON Paths' to auto-create paths like `$.invoices[0].invoice_number`, `$.invoices[0].total`, `$.invoices[0].status`.

get_invoices_config.json
1{
2 "method": "GET",
3 "endpoint": "/invoices?organization_id=[organizationId]&sort_column=date&sort_order=D&per_page=50",
4 "headers": {
5 "Authorization": "Zoho-oauthtoken [accessToken]",
6 "Content-Type": "application/json"
7 }
8}
9

Pro tip: Zoho Books uses `sort_order=D` for descending and `A` for ascending — not 'desc'/'asc'. The API returns `$.invoices` for invoice lists, `$.contacts` for customer lists, and `$.invoice` (singular) for a single invoice. Always double-check which resource key applies to the endpoint you're calling.

Expected result: The ZohoBooks API Group shows getOrganizations, getInvoices, getCustomers, and createInvoice calls. Testing getOrganizations in the Response & Test tab returns a 200 with `$.organizations` containing your Zoho Books org ID.

4

Bootstrap: fetch token and organization_id on app launch

This is the most critical architectural step: both the access token and the organization ID must be fetched before any other Zoho Books API call can succeed. First, set up App State. Click the puzzle-piece icon in the left nav → App State. Add three String variables: - `zbAccessToken` — stores the 1-hour access token (initial value empty) - `zbOrganizationId` — stores the Zoho Books org ID (initial value empty) - `zbTokenCreatedAt` — stores the timestamp of when the token was fetched (initial value empty, optional but recommended) Create the token-service API Group. Left nav → API Calls → '+ Add' → 'Create API Group'. Name it 'ZohoBooksAuth'. Set Base URL to your Firebase Cloud Function's HTTPS trigger URL. Add API Call: Method GET, name `getToken`, endpoint `/getZohoBooksToken`. Now wire the startup sequence to the initial page's 'On Page Load' trigger: 1. Action 1 — 'Backend/API Call' → `getToken` from ZohoBooksAuth. Map `$.access_token` → App State `zbAccessToken`. 2. Action 2 (chained after Action 1 succeeds) — 'Backend/API Call' → `getOrganizations` from ZohoBooks. Pass `App State → zbAccessToken` as `accessToken`. Map `$.organizations[0].organization_id` → App State `zbOrganizationId`. Chaining these two actions in sequence is essential: the `getOrganizations` call needs a valid token (from Action 1) and must complete before any other page can make Books API calls. Add a loading indicator on the initial page that shows while these two calls complete, then navigate to the main screen. For all subsequent Zoho Books API Calls in your app, pass `App State → zbAccessToken` as `accessToken` and `App State → zbOrganizationId` as `organizationId`.

Pro tip: Store the `zbOrganizationId` in App State (not Page State) so it persists across all pages for the lifetime of the app session. Re-fetching the org ID on every page load is wasteful and consumes your limited daily API call budget unnecessarily.

Expected result: After app launch, both `App State → zbAccessToken` and `App State → zbOrganizationId` are non-empty. Logging or displaying them confirms a valid token and a numeric-looking organization ID string.

5

Build an invoices ListView and handle daily API call limits

With the token and organization ID in App State, build the invoice listing UI. On your main page, drag a ListView widget onto the canvas. Select it → Backend Query → API Call → `getInvoices` from ZohoBooks. Under Variables, pass `App State → zbAccessToken` as `accessToken` and `App State → zbOrganizationId` as `organizationId`. Under Response JSON Path, enter `$.invoices`. Inside the ListView, add a Column. Bind Text widgets to: - Invoice number: `$.invoice_number` - Customer name: `$.customer_name` - Total: `$.total` (format as currency using a custom Format function or just display as-is) - Due date: `$.due_date` - Status: `$.status` — add a Container with conditional background color: green for 'paid', orange for 'sent', red for 'overdue' For the invoice detail view, add a new page. On navigation (when the user taps a ListView item), pass the invoice's `$.invoice_id` as a page parameter. Use a `getInvoiceById` API Call: `GET /invoices/[invoiceId]?organization_id=[orgId]`, parse `$.invoice` (singular). Display all line items using a nested Column. Crucially, disable auto-refresh on the ListView. Zoho Books has a 2,500 calls/day free limit and 5,000 on paid plans. Even at 25 invoices per fetch, that is only 100–200 list refreshes per day before you hit the cap. Instead, add a 'pull-to-refresh' gesture that triggers an explicit re-fetch, and cache the result in an App State List<Dynamic> variable between navigations. For the 'mark as paid' action, use a POST to `/invoices/[invoiceId]/payments?organization_id=[orgId]` with a JSON body: `{"invoice_id":"[invoiceId]","amount":[totalDue],"payment_mode":"cash","date":"[today]"}`. After success, refresh the invoice detail from the API to show the updated status. If you're building this for a growing team and want to avoid managing the proxy and rate-limit logic yourself, RapidDev's team builds FlutterFlow accounting integrations regularly — book a free scoping call at rapidevelopers.com/contact.

record_payment_config.json
1{
2 "method": "POST",
3 "endpoint": "/invoices/[invoiceId]/payments?organization_id=[organizationId]",
4 "headers": {
5 "Authorization": "Zoho-oauthtoken [accessToken]",
6 "Content-Type": "application/json"
7 },
8 "body": {
9 "invoice_id": "[invoiceId]",
10 "amount": "[amountPaid]",
11 "payment_mode": "cash",
12 "date": "[paymentDate]"
13 }
14}
15

Pro tip: Zoho Books responses include a `$.page_context` object with `has_more_page` (boolean) and `page` (current page number). Use these to implement pagination — show 'Load More' only when `has_more_page` is true, and append the next page's `$.invoices` array to the existing list in App State.

Expected result: The invoices ListView shows real Zoho Books invoices in FlutterFlow Run mode with correct status badges. The daily API call counter in Zoho Books Settings remains within limits after normal use.

Common use cases

Mobile invoice viewer — check invoice status and payment history on the go

A FlutterFlow app lets business owners and accounts receivable staff browse outstanding invoices from Zoho Books, see payment status, and view invoice totals — from a phone or tablet without logging into the Zoho web app. Invoices are fetched from the Books v3 API and displayed in a list sorted by due date.

FlutterFlow Prompt

Build a Flutter mobile app that fetches invoices from Zoho Books and displays them in a list with invoice number, customer name, total amount, due date, and status. Tapping an invoice shows full line items. Include a filter for overdue invoices.

Copy this prompt to try it in FlutterFlow

Field service billing app — create invoices after a job is completed

A FlutterFlow app for field service technicians lets them create a Zoho Books invoice directly after completing a job. The tech selects the customer from a Books customer list, enters line items and quantities, and taps 'Create Invoice'. The invoice is POSTed to Zoho Books and immediately visible to the accounting team.

FlutterFlow Prompt

Build a Flutter invoice-creation app connected to Zoho Books. Show a customer search/select, allow adding multiple line items with description and price, and POST a new invoice to Zoho Books on submit. Display the created invoice number as confirmation.

Copy this prompt to try it in FlutterFlow

Payment operations dashboard — mark invoices as paid from a tablet

A FlutterFlow tablet app for an ops manager shows a list of sent invoices from Zoho Books. The manager can tap an invoice and select 'Mark Paid', which records a payment in Zoho Books via a POST to the invoice's payment endpoint. This keeps the accounting system current without the manager needing to log into Zoho Books.

FlutterFlow Prompt

Build a Flutter tablet operations app showing outstanding Zoho Books invoices. Include a detail view for each invoice with a 'Record Payment' button that posts a payment record to Zoho Books and updates the invoice status to 'paid'.

Copy this prompt to try it in FlutterFlow

Troubleshooting

Every Zoho Books API call returns HTTP 400 — no data returned

Cause: The `organization_id` query parameter is missing from the API Call endpoint. Zoho Books requires this parameter on every request except `GET /organizations`.

Solution: In every Zoho Books API Call endpoint string in FlutterFlow, ensure `?organization_id=[organizationId]` is appended. Verify that the `organizationId` variable is being passed correctly from App State when the API Call is triggered. Use the Response & Test tab to test the call with a known organization ID value to confirm it works before wiring it to App State.

API calls return 401 or `INVALID_TOKEN` one hour after app launch

Cause: Zoho Books access tokens expire after exactly 3,600 seconds (1 hour). If the app session extends past that point without refreshing the token, all subsequent API calls fail with 401.

Solution: Add token refresh logic to frequently-visited pages. In the 'On Page Load' action of any page the user might be on for extended periods, add a Conditional Action: check if more than 3,500 seconds have elapsed since the last token fetch (compare current timestamp to `App State → zbTokenCreatedAt`). If so, call the Firebase Cloud Function to get a new token and update App State before proceeding.

Invoice list is empty — `$.invoices` returns no records

Cause: The JSON Path is correct but the Zoho Books organization has no invoices matching the default query parameters, or the `status` filter is excluding all records.

Solution: In the Response & Test tab for `getInvoices`, test with the actual API token and organization ID. Remove any `status` filter parameters from the endpoint to fetch all invoices first. If results appear there but not in FlutterFlow Run mode, the issue is with how the Variables are being passed — check that `organizationId` is correctly mapped to the App State variable in the Action Flow.

Region mismatch: 401 errors even with a freshly refreshed token

Cause: The Zoho Books API domain (e.g. `zohoapis.eu`) does not match the token endpoint domain used in the Cloud Function (e.g. `accounts.zoho.com`). These must both use the same regional suffix.

Solution: Check your Zoho account's data center: log into your Zoho account at accounts.zoho.com → your profile → data center information. Update the Firebase Cloud Function's `zohobooks.region` config and the FlutterFlow API Group base URL to both use the correct regional suffix. EU accounts must use both `accounts.zoho.eu` (token) and `zohoapis.eu` (API) — mixing them causes 401.

Best practices

  • Always call `GET /organizations` first on app launch and store the `organization_id` in App State — this parameter is mandatory on every subsequent Zoho Books API call, and missing it causes 400 errors across the board.
  • Use the `Zoho-oauthtoken` authorization header prefix exactly — Zoho Books rejects standard `Bearer` prefix with 401, the same as Zoho CRM. Copy-paste the prefix rather than typing it to avoid case-sensitivity issues.
  • Keep the refresh token in Firebase Cloud Function environment variables only — never in FlutterFlow App State, App Values, or Dart code. The refresh token is a permanent credential; the access token is the short-lived one safe to store in App State.
  • Cache invoice and customer list results in App State between page navigations. With only 2,500–5,000 API calls per day, every unthrottled list refresh counts — treat the daily budget like a limited resource.
  • Match both the Zoho API domain and the Zoho OAuth token domain to the same region (both `.com`, `.eu`, `.in`, or `.com.au`) — the most common source of mysterious 401 errors is a region mismatch between the two.
  • Scope your self-client grant to only the Zoho Books modules you access (e.g. `ZohoBooks.invoices.READ` + `ZohoBooks.customers.READ` rather than `ZohoBooks.fullaccess.all`) — this limits blast radius if the refresh token is ever compromised.
  • Include `ZohoBooks.settings.READ` in your OAuth scopes even if you don't plan to modify settings — this scope is required to call the `/organizations` bootstrap endpoint that fetches the mandatory organization_id.
  • Test your integration against a Zoho Books trial organization with dummy invoices before connecting to a production accounting database — mistakes in record creation or payment posting cannot always be easily reversed.

Alternatives

Frequently asked questions

Can I use the same Zoho OAuth self-client for both Zoho CRM and Zoho Books?

Yes. A single self-client in the Zoho API Console can be granted scopes for multiple Zoho services. Include both ZohoCRM and ZohoBooks scopes when generating the grant code: `ZohoCRM.modules.leads.ALL,ZohoBooks.invoices.READ,ZohoBooks.settings.READ`. The refresh token returned covers all granted scopes. However, keep the FlutterFlow API Groups and Firebase Cloud Functions separate (or use a single function with different endpoints) to keep concerns organized.

Why do I get HTTP 400 even when my token is valid?

The most likely cause is a missing `organization_id` query parameter. Zoho Books returns 400 (not 401) when this mandatory parameter is absent. Verify that every API Call endpoint in your FlutterFlow app includes `?organization_id=[organizationId]` and that the `organizationId` App State variable is populated before any Books API Call runs.

How many Zoho Books API calls does a typical FlutterFlow session use?

A typical session might use: 1 call for token (Cloud Function), 1 for organizations, 1–2 for invoice lists, and a few for detail views. That's roughly 5–10 calls per user session. With 2,500 calls per day on the free plan, you support approximately 250–500 sessions before hitting the limit. If your app has many users, upgrade to a paid Zoho Books plan (5,000 calls/day) or implement aggressive caching to reduce per-session call counts.

Does FlutterFlow have a native Zoho Books connector?

No. FlutterFlow does not include a built-in Zoho Books integration. You connect using the API Calls panel with the Zoho Books v3 REST endpoints configured manually. The Zoho OAuth self-client provides server-to-server access, but the refresh token requires a Firebase Cloud Function proxy to avoid embedding it in the app bundle.

Can I use Zoho Books with a FlutterFlow web app?

Yes, but web builds may encounter CORS issues if your browser blocks cross-origin requests to Zoho's APIs directly. If that happens, route all API calls through the Firebase Cloud Function proxy (not just the token endpoint), which sets CORS headers and avoids browser restrictions. Mobile builds (iOS/Android) are not affected by CORS.

What is the difference between Zoho Books and Zoho CRM in terms of FlutterFlow integration?

Both use the same Zoho OAuth self-client and `Zoho-oauthtoken` header. The differences are: Zoho Books requires `organization_id` on every API call (CRM does not); Zoho Books base URL is `/books/v3` (CRM is `/crm/v7`); Zoho Books has lower daily call limits (2,500–5,000/day) vs CRM's per-hour credits; Zoho Books targets financial records (invoices, payments) while CRM targets sales records (Leads, Deals). The OAuth refresh token itself can be shared between the two if the same self-client is used.

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 FlutterFlow 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.