# Zoho Books

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

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

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

## Before you start

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

```
// Exchange grant code for refresh token (run once, e.g. in browser console or Postman)
POST https://accounts.zoho.com/oauth/v2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&code=YOUR_GRANT_CODE
&redirect_uri=
```

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

```
const functions = require('firebase-functions');
const axios = require('axios');

exports.getZohoBooksToken = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') {
    res.set('Access-Control-Allow-Methods', 'GET');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
    return res.status(204).send('');
  }

  const config = functions.config().zohobooks;
  const tokenUrl = `https://accounts.zoho.${config.region}/oauth/v2/token`;

  try {
    const params = new URLSearchParams();
    params.append('refresh_token', config.refresh_token);
    params.append('client_id', config.client_id);
    params.append('client_secret', config.client_secret);
    params.append('grant_type', 'refresh_token');

    const response = await axios.post(tokenUrl, params);
    return res.json({
      access_token: response.data.access_token,
      expires_in: response.data.expires_in
    });
  } catch (err) {
    console.error('Zoho Books token error:', err.response?.data || err.message);
    return res.status(500).json({ error: 'Token refresh failed' });
  }
});
```

**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`.

```
{
  "method": "GET",
  "endpoint": "/invoices?organization_id=[organizationId]&sort_column=date&sort_order=D&per_page=50",
  "headers": {
    "Authorization": "Zoho-oauthtoken [accessToken]",
    "Content-Type": "application/json"
  }
}
```

**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`.

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

```
{
  "method": "POST",
  "endpoint": "/invoices/[invoiceId]/payments?organization_id=[organizationId]",
  "headers": {
    "Authorization": "Zoho-oauthtoken [accessToken]",
    "Content-Type": "application/json"
  },
  "body": {
    "invoice_id": "[invoiceId]",
    "amount": "[amountPaid]",
    "payment_mode": "cash",
    "date": "[paymentDate]"
  }
}
```

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

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

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

Prompt example:

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

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

Prompt example:

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

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

Prompt example:

```
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'.
```

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

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

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/zoho-books
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/zoho-books
