Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Zoho Books

Integrate Bolt.new with Zoho Books by creating a Zoho API client in the Zoho Developer Console, implementing OAuth 2.0 (requires deployment for the callback URL), then calling Zoho Books' REST API from Next.js API routes. Zoho Books has a free plan for businesses under $50K annual revenue and integrates natively with 45+ other Zoho apps. Store credentials in .env, never in client-side code.

What you'll learn

  • How to create a Zoho API client in the Zoho Developer Console and obtain OAuth 2.0 credentials
  • How to implement the Zoho OAuth 2.0 authorization flow in Next.js API routes
  • How to call Zoho Books REST API endpoints for invoices, contacts, and expenses
  • How Zoho Books integrates with Zoho CRM, Zoho Inventory, and other Zoho apps
  • How to handle Zoho's organization ID and multi-region API endpoint requirements
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read40 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

Integrate Bolt.new with Zoho Books by creating a Zoho API client in the Zoho Developer Console, implementing OAuth 2.0 (requires deployment for the callback URL), then calling Zoho Books' REST API from Next.js API routes. Zoho Books has a free plan for businesses under $50K annual revenue and integrates natively with 45+ other Zoho apps. Store credentials in .env, never in client-side code.

Building Accounting Features with Zoho Books' REST API

Zoho Books occupies a unique position in the accounting software landscape: it is part of a 45+ application ecosystem (Zoho One) that spans CRM, email, HR, project management, inventory, and more. For businesses already using Zoho CRM, Zoho Books is the natural accounting companion because data flows automatically between the two systems — CRM contacts become accounting contacts, CRM deals become Books invoices, and payment status syncs back to the CRM. If your Bolt app serves businesses in the Zoho ecosystem, Zoho Books integration unlocks this full data pipeline.

Zoho Books also has a genuinely free tier: businesses with annual revenue under $50,000 USD can use the core accounting features at no cost. This is unique among major accounting platforms and makes Zoho Books the accessible starting point for very small business tools without requiring your users to pay for accounting software. The free tier includes the full API, so you can integrate without your users needing a paid Zoho subscription.

The Zoho Books REST API is comprehensive and follows standard REST conventions — resource-based URLs with HTTP verbs, JSON bodies, and clear error codes. Authentication uses OAuth 2.0, consistent with the rest of the Zoho platform, so if you are already integrating with Zoho CRM or Zoho Projects, the same OAuth client works for Zoho Books. The main Zoho-specific considerations are the organization ID (similar to Xero's tenant ID) and Zoho's multi-region data centers — API endpoints differ between US, EU, IN, AU, and JP regions.

Integration method

Bolt Chat + API Route

Bolt generates the Zoho Books integration code — OAuth 2.0 authorization routes, API handlers for invoices and contacts, and dashboard components — through conversation with the AI. Zoho Books uses OAuth 2.0, requiring users to authorize your app through Zoho's consent screen. The OAuth callback needs a public HTTPS URL, so you must deploy before testing the full auth flow. All API calls go through server-side Next.js routes to protect credentials.

Prerequisites

  • A Zoho account (free at zoho.com — no credit card required for the free Zoho Books tier)
  • A Zoho API client created in the Zoho Developer Console (accounts.zoho.com/developerconsole) with OAuth 2.0 credentials
  • Your Zoho Books organization ID (visible in Zoho Books Settings → Organization Profile, or from the API after auth)
  • A deployed Bolt.new app on Netlify or Bolt Cloud (required for the OAuth redirect URI)
  • A Next.js project in Bolt — Zoho Books has no official Node.js SDK so you will use the REST API directly

Step-by-step guide

1

Create a Zoho API client and obtain OAuth 2.0 credentials

Zoho uses a centralized developer console for all Zoho APIs — the same client credentials work for Zoho CRM, Zoho Books, Zoho Mail, and all other Zoho products. Go to accounts.zoho.com/developerconsole (you must be logged into your Zoho account). Click Add Client and select Web Based as the client type. Fill in the form: client name (shown to users during authorization), homepage URL (your app's URL), and authorized redirect URIs. Add your deployed URL (e.g., https://your-app.netlify.app/api/zoho/callback) and http://localhost:3000/api/zoho/callback for local testing. Click Create. After creation, you see the Client ID and Client Secret. The Client Secret must only be used server-side. Also note the Zoho region for your account — Zoho has separate data centers for US (accounts.zoho.com), EU (accounts.zoho.eu), India (accounts.zoho.in), Australia (accounts.zoho.com.au), and Japan (accounts.zoho.jp). Your OAuth endpoints and API URLs must match the region where your Zoho account is hosted. For Zoho Books API calls, the base URL is region-specific: https://books.zoho.com/api/v3 for US, https://books.zoho.eu/api/v3 for EU, and so on. Use the same region as your Zoho account. Store the region in your .env file as ZOHO_REGION so you can construct the correct API URLs. To find your Zoho Books organization ID: after completing OAuth and connecting your Zoho Books account, call the /organizations endpoint to list all organizations associated with the token. The organization ID is required for every Zoho Books API call as a query parameter.

Bolt.new Prompt

Set up Zoho Books OAuth 2.0 in my Next.js app. Create lib/zoho-auth.ts exporting a function to generate the Zoho auth URL using ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, ZOHO_REDIRECT_URI, and ZOHO_REGION from .env. The auth URL should request scopes: ZohoBooks.invoices.READ, ZohoBooks.invoices.CREATE, ZohoBooks.contacts.READ, ZohoBooks.expenses.READ, ZohoBooks.expenses.CREATE. Create /api/zoho/authorize that redirects to the auth URL.

Paste this in Bolt.new chat

lib/zoho-auth.ts
1// .env.local
2ZOHO_CLIENT_ID=your_zoho_client_id
3ZOHO_CLIENT_SECRET=your_zoho_client_secret
4ZOHO_REDIRECT_URI=https://your-app.netlify.app/api/zoho/callback
5ZOHO_REGION=com # com, eu, in, com.au, jp
6ZOHO_BOOKS_ORG_ID=your_organization_id # Set after first OAuth
7
8// lib/zoho-auth.ts
9export function getZohoRegion(): string {
10 return process.env.ZOHO_REGION ?? 'com';
11}
12
13export function getZohoAuthBaseUrl(): string {
14 return `https://accounts.zoho.${getZohoRegion()}`;
15}
16
17export function getZohoBooksBaseUrl(): string {
18 return `https://books.zoho.${getZohoRegion()}/api/v3`;
19}
20
21export function buildAuthUrl(state: string): string {
22 const baseUrl = getZohoAuthBaseUrl();
23 const params = new URLSearchParams({
24 response_type: 'code',
25 client_id: process.env.ZOHO_CLIENT_ID!,
26 scope: [
27 'ZohoBooks.invoices.READ',
28 'ZohoBooks.invoices.CREATE',
29 'ZohoBooks.contacts.READ',
30 'ZohoBooks.contacts.CREATE',
31 'ZohoBooks.expenses.READ',
32 'ZohoBooks.expenses.CREATE',
33 ].join(','),
34 redirect_uri: process.env.ZOHO_REDIRECT_URI!,
35 access_type: 'offline',
36 state,
37 });
38 return `${baseUrl}/oauth/v2/auth?${params.toString()}`;
39}
40
41// app/api/zoho/authorize/route.ts
42import { NextResponse } from 'next/server';
43import { buildAuthUrl } from '@/lib/zoho-auth';
44
45export async function GET() {
46 const state = Math.random().toString(36).substring(2, 15);
47 const authUrl = buildAuthUrl(state);
48 const response = NextResponse.redirect(authUrl);
49 response.cookies.set('zoho_oauth_state', state, { httpOnly: true, maxAge: 600 });
50 return response;
51}

Pro tip: Zoho uses scope names in the format ZohoBooks.{resource}.{permission} — for example ZohoBooks.invoices.READ and ZohoBooks.invoices.CREATE are separate scopes. Request the minimum scopes your app needs. The full scope list is at zoho.com/books/api/v3/introduction/scopes.

Expected result: Visiting /api/zoho/authorize redirects to Zoho's login and consent screen. After logging in, users see your app name and the requested Zoho Books permissions before clicking Allow.

2

Handle the OAuth callback and exchange the code for tokens

After the user authorizes your app, Zoho redirects to your callback URL with an authorization code. Your callback route must exchange this code for access and refresh tokens by calling Zoho's token endpoint. Zoho's token endpoint is at https://accounts.zoho.{region}/oauth/v2/token. Send a POST request with the authorization code, client credentials, redirect URI, and grant_type: 'authorization_code'. Zoho returns an access_token (expires in 1 hour), a refresh_token (long-lived), and the token_type and expires_in fields. After obtaining tokens, call the Zoho Books organizations endpoint to retrieve the organization ID. Every Zoho Books API call requires the organization_id as a query parameter — there is no default. If the user has multiple Zoho Books organizations (common for accountants), list them and either pick the first or implement an organization selector. Store the access token, refresh token, token expiry, and organization ID in server-side cookies or a database. The access token expires in 3600 seconds (1 hour). When making API calls, check if the token is within 5 minutes of expiry and refresh it using the refresh_token grant type before proceeding. Critical: Zoho OAuth callbacks require a deployed URL. The WebContainer cannot receive incoming redirects from Zoho's servers. Deploy to Netlify or Bolt Cloud first, register the deployed URL in the Zoho Developer Console, and test auth on the deployed app.

Bolt.new Prompt

Create the Zoho OAuth callback at /api/zoho/callback. Exchange the code for tokens by POST to the Zoho token endpoint with client credentials. After getting tokens, call the Zoho Books /organizations endpoint with the access token to get the organization ID. Store access_token, refresh_token, expiry timestamp (Date.now() + expires_in * 1000), and organization_id in HTTP-only cookies. Redirect to /dashboard on success. Create lib/zoho-api.ts with a zohoBooksCall helper that reads tokens from cookies, refreshes if expired, and calls the Zoho Books API.

Paste this in Bolt.new chat

app/api/zoho/callback/route.ts
1// app/api/zoho/callback/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getZohoAuthBaseUrl, getZohoBooksBaseUrl } from '@/lib/zoho-auth';
4
5export async function GET(request: NextRequest) {
6 const { searchParams } = new URL(request.url);
7 const code = searchParams.get('code');
8 const error = searchParams.get('error');
9
10 if (error || !code) {
11 return NextResponse.redirect(new URL(`/error?message=${error ?? 'no_code'}`, request.url));
12 }
13
14 try {
15 // Exchange code for tokens
16 const tokenResponse = await fetch(`${getZohoAuthBaseUrl()}/oauth/v2/token`, {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
19 body: new URLSearchParams({
20 code,
21 client_id: process.env.ZOHO_CLIENT_ID!,
22 client_secret: process.env.ZOHO_CLIENT_SECRET!,
23 redirect_uri: process.env.ZOHO_REDIRECT_URI!,
24 grant_type: 'authorization_code',
25 }).toString(),
26 });
27
28 const tokens = await tokenResponse.json() as {
29 access_token: string;
30 refresh_token: string;
31 expires_in: number;
32 token_type: string;
33 };
34
35 if (!tokens.access_token) {
36 throw new Error('No access token in response');
37 }
38
39 // Fetch organization ID
40 const orgsResponse = await fetch(`${getZohoBooksBaseUrl()}/organizations`, {
41 headers: { Authorization: `Zoho-oauthtoken ${tokens.access_token}` },
42 });
43 const orgsData = await orgsResponse.json() as { organizations: Array<{ organization_id: string; name: string }> };
44 const orgId = orgsData.organizations[0]?.organization_id;
45
46 if (!orgId) throw new Error('No Zoho Books organization found');
47
48 const response = NextResponse.redirect(new URL('/dashboard', request.url));
49 const cookieOpts = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, maxAge: 60 * 60 * 24 * 30, path: '/' };
50 response.cookies.set('zoho_access_token', tokens.access_token, { ...cookieOpts, maxAge: tokens.expires_in });
51 response.cookies.set('zoho_refresh_token', tokens.refresh_token, cookieOpts);
52 response.cookies.set('zoho_token_expiry', String(Date.now() + tokens.expires_in * 1000), cookieOpts);
53 response.cookies.set('zoho_org_id', orgId, cookieOpts);
54
55 return response;
56 } catch (err) {
57 console.error('Zoho OAuth error:', err);
58 return NextResponse.redirect(new URL('/error?message=zoho_auth_failed', request.url));
59 }
60}

Pro tip: Zoho refresh tokens do not expire as long as you use them at least once every 3 months. Unlike some other platforms, Zoho does not issue a new refresh token when you use it to get a new access token — the same refresh token remains valid indefinitely with regular use.

Expected result: After authorizing on Zoho's consent screen, you are redirected to /dashboard with zoho_access_token, zoho_refresh_token, zoho_token_expiry, and zoho_org_id cookies set. The organization ID is stored and ready for API calls.

3

Fetch invoices and contacts from the Zoho Books API

Zoho Books REST API follows standard REST conventions with clear resource paths. The base URL is https://books.zoho.{region}/api/v3. Every request requires the organization_id as a query parameter and the Authorization header as Zoho-oauthtoken {access_token}. Create a reusable API helper that handles authentication (reading tokens from cookies, refreshing if expired) and constructs the correct URL with organization_id automatically appended. This reduces boilerplate in your individual API routes. For invoices, GET /invoices returns a paginated list with filtering support. Useful filter parameters: status (draft, sent, overdue, paid, void), customer_id, date_start, date_end, and sort_column (date, invoice_number, total). The response includes invoice_id, invoice_number, customer_name, date, due_date, total, balance (outstanding amount), and status. For contacts (customers and suppliers), GET /contacts returns the full contact list. Filter by contact_type=customer for invoice customers only. Contact records include contact_id, contact_name, email, phone, billing_address, and outstanding_receivable_amount (the total unpaid balance across all invoices for that customer). Zoho Books API rate limits: 100 requests per minute for paid plans, 60 requests per minute for the free plan. Use caching for data that does not change frequently, and avoid making API calls on every component render.

Bolt.new Prompt

Create a lib/zoho-api.ts utility with a zohoBooksCall function that reads zoho_access_token, zoho_token_expiry, zoho_refresh_token, and zoho_org_id from cookies, refreshes the token if within 5 minutes of expiry, and calls the Zoho Books API with the correct Authorization header and organization_id query parameter. Then create /api/zoho-books/invoices route that calls GET /invoices with status=overdue,sent as the default filter and returns normalized invoice objects with id, invoiceNumber, customerName, date, dueDate, total, balance, status, and daysOverdue.

Paste this in Bolt.new chat

lib/zoho-api.ts
1// lib/zoho-api.ts
2import { cookies } from 'next/headers';
3import { getZohoAuthBaseUrl, getZohoBooksBaseUrl } from './zoho-auth';
4
5async function refreshAccessToken(refreshToken: string): Promise<{ access_token: string; expires_in: number }> {
6 const response = await fetch(`${getZohoAuthBaseUrl()}/oauth/v2/token`, {
7 method: 'POST',
8 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
9 body: new URLSearchParams({
10 refresh_token: refreshToken,
11 client_id: process.env.ZOHO_CLIENT_ID!,
12 client_secret: process.env.ZOHO_CLIENT_SECRET!,
13 grant_type: 'refresh_token',
14 }).toString(),
15 });
16 return response.json();
17}
18
19export async function zohoBooksCall<T>(
20 path: string,
21 options: RequestInit = {}
22): Promise<T> {
23 const cookieStore = cookies();
24 let accessToken = cookieStore.get('zoho_access_token')?.value;
25 const refreshToken = cookieStore.get('zoho_refresh_token')?.value;
26 const tokenExpiry = parseInt(cookieStore.get('zoho_token_expiry')?.value ?? '0', 10);
27 const orgId = cookieStore.get('zoho_org_id')?.value;
28
29 if (!accessToken || !orgId) throw new Error('Not authenticated with Zoho Books');
30
31 // Refresh if within 5 minutes of expiry
32 if (refreshToken && tokenExpiry - Date.now() < 5 * 60 * 1000) {
33 const fresh = await refreshAccessToken(refreshToken);
34 accessToken = fresh.access_token;
35 }
36
37 const url = new URL(`${getZohoBooksBaseUrl()}${path}`);
38 url.searchParams.set('organization_id', orgId);
39
40 const response = await fetch(url.toString(), {
41 ...options,
42 headers: {
43 Authorization: `Zoho-oauthtoken ${accessToken}`,
44 'Content-Type': 'application/json',
45 ...(options.headers ?? {}),
46 },
47 });
48
49 if (!response.ok) {
50 throw new Error(`Zoho Books API error: ${response.status}`);
51 }
52
53 return response.json() as Promise<T>;
54}
55
56// app/api/zoho-books/invoices/route.ts
57import { NextRequest, NextResponse } from 'next/server';
58import { zohoBooksCall } from '@/lib/zoho-api';
59
60interface ZohoInvoice {
61 invoice_id: string;
62 invoice_number: string;
63 customer_name: string;
64 date: string;
65 due_date: string;
66 total: number;
67 balance: number;
68 status: string;
69}
70
71export async function GET(request: NextRequest) {
72 const { searchParams } = new URL(request.url);
73 const status = searchParams.get('status') ?? 'sent,overdue';
74
75 try {
76 const data = await zohoBooksCall<{ invoices: ZohoInvoice[]; page_context: Record<string, unknown> }>(
77 `/invoices?status=${encodeURIComponent(status)}&sort_column=date&sort_order=D&per_page=200`
78 );
79
80 const today = new Date();
81 const invoices = data.invoices.map((inv) => {
82 const dueDate = inv.due_date ? new Date(inv.due_date) : null;
83 const daysOverdue = dueDate && dueDate < today && inv.balance > 0
84 ? Math.floor((today.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24))
85 : 0;
86 return {
87 id: inv.invoice_id,
88 invoiceNumber: inv.invoice_number,
89 customerName: inv.customer_name,
90 date: inv.date,
91 dueDate: inv.due_date,
92 total: inv.total,
93 balance: inv.balance,
94 status: inv.status,
95 daysOverdue,
96 };
97 });
98
99 return NextResponse.json({ invoices, total: invoices.length });
100 } catch (error) {
101 const message = error instanceof Error ? error.message : 'Failed to fetch invoices';
102 return NextResponse.json({ error: message }, { status: 500 });
103 }
104}

Pro tip: Zoho Books API pagination uses page and per_page parameters (max 200 per page). For a dashboard showing recent invoices, per_page=200 is usually sufficient. For comprehensive reports across all historical invoices, implement pagination by checking the page_context.has_more_page field and incrementing the page number.

Expected result: Calling /api/zoho-books/invoices returns unpaid invoices from your Zoho Books account with balance amounts, customer names, due dates, and days overdue calculated.

4

Create invoices and manage expenses via the API

Zoho Books API supports creating and updating invoices programmatically. The POST /invoices endpoint accepts the complete invoice data: customer_id, invoice_number (optional, auto-assigned if omitted), date, due_date, line_items (description, quantity, rate, account_id), notes, terms, and more. To create an invoice for a customer, you need their customer_id. Fetch the customer list from GET /contacts?contact_type=customer and either match by email address or let users select from a dropdown. The customer_id is a string UUID assigned by Zoho Books. Each line item requires at minimum: description, rate (unit price), and quantity. The account_id determines which chart of accounts category the revenue posts to — fetch your available income accounts from GET /chartofaccounts?account_type=income to get valid IDs. If you omit account_id, Zoho uses the default income account. For expenses, POST /expenses with vendor_id (or just the vendor_name for non-recurring expenses), date, amount, account_id (the expense account category), and optionally a description and reference_number. Expense accounts can be fetched from GET /chartofaccounts?account_type=expense. Both invoice creation and expense creation work as outbound HTTP calls from your Next.js API routes, so they function correctly in Bolt's WebContainer preview for testing. The exception is PDF invoice delivery — sending an invoice email to a customer should be tested with a test email address in development to avoid sending premature invoices to real customers.

Bolt.new Prompt

Create a /api/zoho-books/invoices/create route that accepts POST with customerId, lineItems (array of description, quantity, rate), dueDate, and optional notes. Call Zoho Books POST /invoices with the invoice data. Return the created invoice_id, invoice_number, total, and a link to view the invoice in Zoho Books. Also create /api/zoho-books/expenses/create that accepts POST with vendor_name, amount, expenseAccountId, date, and description. Call Zoho Books POST /expenses and return the created expense_id.

Paste this in Bolt.new chat

app/api/zoho-books/invoices/create/route.ts
1// app/api/zoho-books/invoices/create/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { zohoBooksCall } from '@/lib/zoho-api';
4
5export async function POST(request: NextRequest) {
6 const body = await request.json() as {
7 customerId: string;
8 lineItems: Array<{ description: string; quantity: number; rate: number }>;
9 dueDate: string;
10 notes?: string;
11 sendEmail?: boolean;
12 };
13
14 try {
15 const invoicePayload = {
16 customer_id: body.customerId,
17 date: new Date().toISOString().split('T')[0],
18 due_date: body.dueDate,
19 notes: body.notes ?? '',
20 line_items: body.lineItems.map((item) => ({
21 description: item.description,
22 quantity: item.quantity,
23 rate: item.rate,
24 })),
25 };
26
27 const data = await zohoBooksCall<{
28 invoice: {
29 invoice_id: string;
30 invoice_number: string;
31 total: number;
32 status: string;
33 };
34 }>('/invoices', {
35 method: 'POST',
36 body: JSON.stringify(invoicePayload),
37 });
38
39 const inv = data.invoice;
40
41 // Optionally send the invoice email
42 if (body.sendEmail) {
43 await zohoBooksCall(`/invoices/${inv.invoice_id}/email`, { method: 'POST' });
44 }
45
46 return NextResponse.json({
47 invoiceId: inv.invoice_id,
48 invoiceNumber: inv.invoice_number,
49 total: inv.total,
50 status: inv.status,
51 viewUrl: `https://books.zoho.${process.env.ZOHO_REGION ?? 'com'}/app#/invoices/${inv.invoice_id}`,
52 });
53 } catch (error) {
54 const message = error instanceof Error ? error.message : 'Invoice creation failed';
55 return NextResponse.json({ error: message }, { status: 500 });
56 }
57}

Pro tip: Zoho Books uses ISO date format (YYYY-MM-DD) for all dates. Always format dates as strings in this format before sending to the API. JavaScript's new Date().toISOString().split('T')[0] gives you today's date in the correct format.

Expected result: POST /api/zoho-books/invoices/create creates a real invoice in Zoho Books and returns the invoice ID, number, and total. The invoice appears in the Zoho Books interface and can be sent to the customer.

Common use cases

Invoice management dashboard with Zoho CRM customer sync

Build a combined dashboard that shows open invoices from Zoho Books alongside the customer records from Zoho CRM. Sales reps see invoice status for their deals without needing to switch between two Zoho apps. Clicking an invoice links to the full Zoho Books record.

Bolt.new Prompt

Create a Next.js app integrating Zoho Books and optionally Zoho CRM. Build an invoice dashboard that fetches all unpaid invoices from the Zoho Books API with customer name, amount, due date, and status. Display them as cards sorted by due date. For each customer, fetch their email and phone from Zoho Books contacts. Add a Create Invoice button that opens a form to create a new invoice with customer selection from the contacts list.

Copy this prompt to try it in Bolt.new

Expense tracking and approval workflow

Create an expense submission tool where team members submit expense reports, which are automatically created as expenses in Zoho Books pending approval. Managers see a pending expenses list and can approve or reject, updating the status in Zoho Books via the API.

Bolt.new Prompt

Build an expense tracking app connected to Zoho Books. Create an expense submission form with vendor name, amount, category, date, and receipt upload. When submitted, create an expense in Zoho Books using the expenses API endpoint. Build a manager view showing pending expenses fetched from Zoho Books. Add approve and reject actions that update the expense status. Use ZOHO_BOOKS_ORG_ID and ZOHO_CLIENT credentials from .env.

Copy this prompt to try it in Bolt.new

Revenue and cashflow monitoring widget

Embed a financial summary widget in an internal operations dashboard showing total invoiced this month, total collected, outstanding receivables, and a 6-month revenue trend chart — all from Zoho Books data. Finance teams see the financial picture without a separate Zoho login.

Bolt.new Prompt

Build a ZohoBooksFinancialWidget React component that calls a /api/zoho-books/summary route. The summary route should fetch invoices from Zoho Books for the last 6 months and calculate: total invoiced per month, total collected per month, current outstanding balance, and overdue amount. Display monthly invoiced vs collected as a bar chart using Recharts. Show summary cards for outstanding and overdue amounts.

Copy this prompt to try it in Bolt.new

Troubleshooting

Zoho OAuth redirect shows 'Invalid client_id' error on Zoho's consent screen

Cause: The ZOHO_CLIENT_ID in your .env does not match the Client ID from the Zoho Developer Console, or the OAuth client was created in a different Zoho region than the one your account uses.

Solution: Log into accounts.zoho.{your-region}/developerconsole and verify the exact Client ID. Ensure your ZOHO_REGION environment variable matches the region where both your Zoho account and developer console are hosted. US accounts use zoho.com; European accounts use zoho.eu.

API calls return { code: 14, message: 'Invalid value passed for organizationid' }

Cause: The organization_id is missing from the API call query parameters, is incorrect, or does not belong to the authenticated user's account.

Solution: Verify the zoho_org_id cookie is set correctly after OAuth. Test by calling /api/zoho-books/organizations to list all organizations the token has access to. Log the organization_id being passed to confirm it is the correct format (a string of digits, not a UUID).

typescript
1// Fetch all organizations to verify:
2const orgs = await zohoBooksCall('/organizations');
3console.log('Available orgs:', JSON.stringify(orgs));

Token refresh fails with { error: 'invalid_code', error_description: 'Invalid refresh token' }

Cause: The refresh token was stored incorrectly, the Zoho API client was deleted or regenerated in the developer console, or the refresh token was revoked by the user in Zoho's Connected Apps settings.

Solution: Clear the stored cookies and prompt the user to re-authorize through the OAuth flow. Verify the Client ID and Client Secret are still valid in the Zoho Developer Console — regenerating credentials invalidates all existing tokens. Implement a fallback redirect to /api/zoho/authorize when a token refresh fails.

typescript
1// In zohoBooksCall, handle refresh failures:
2try {
3 const fresh = await refreshAccessToken(refreshToken);
4 accessToken = fresh.access_token;
5} catch {
6 throw new Error('ZOHO_TOKEN_EXPIRED'); // Caller redirects to re-auth
7}

Zoho Books API returns status 400 with 'Required field is missing' on invoice creation

Cause: A required field is missing in the invoice POST body. Zoho Books requires at minimum: customer_id, date, and at least one line_item with description and rate.

Solution: Check that customerId maps to a valid Zoho Books customer_id (not a CRM contact ID). Ensure every line item has at least description, quantity, and rate fields. Log the exact request body being sent to confirm all required fields are present.

typescript
1// Minimum valid invoice payload:
2{
3 customer_id: '123456789', // Must be a valid Zoho Books customer ID
4 date: '2026-04-22',
5 line_items: [{
6 description: 'Consulting services',
7 quantity: 1,
8 rate: 150.00
9 }]
10}

Best practices

  • Always deploy before testing OAuth — Zoho's redirect URI enforcement is strict and the WebContainer cannot receive incoming redirects from Zoho's authorization servers.
  • Use the region-appropriate API URLs and always store ZOHO_REGION in your .env. Using the wrong regional endpoint returns authentication errors even with a valid token.
  • Request only the scopes your app needs — Zoho's consent screen is detailed and shows users each permission. Fewer scopes improve trust and conversion.
  • Cache invoice and contact data that does not change frequently. Zoho Books free plan has a 60 requests/minute limit — a page that makes 5+ API calls on load can exhaust this quickly.
  • Always include organization_id in every API call. The zohoBooksCall helper automates this, but if you make direct fetch calls, a missing org ID causes silent failures rather than auth errors.
  • Handle the multi-organization case gracefully — accountants with Zoho Books access to multiple organizations should see an organization selector rather than always defaulting to the first organization.
  • Test with Zoho Books' test organization (create a separate Zoho account with sample data) before integrating live data. Invoice sending and financial record creation should be tested in isolation.

Alternatives

Frequently asked questions

Is Zoho Books free to use?

Zoho Books has a free plan for businesses with annual revenue under $50,000 USD, limited to one user and one accountant. The free plan includes invoicing, expense tracking, and the full REST API. Paid plans start at $20/month for more users, multi-currency, and advanced features. The API access tier matches the plan — API calls are available on all plans including free.

Can I test the Zoho Books integration in Bolt's preview?

Partially. Outbound API calls to fetch and create Zoho Books data work in the WebContainer once you have valid tokens. The OAuth authorization flow requires deployment since it relies on an incoming redirect from Zoho. Complete auth on your deployed app, then use those tokens for testing API features in development.

How does Zoho Books integrate with Zoho CRM?

Zoho Books and Zoho CRM share contact records — a Zoho Books customer is the same entity as a Zoho CRM contact in most configurations. You can use the same OAuth client for both APIs by adding ZohoCRM scopes alongside ZohoBooks scopes. Zoho's native sync (configured in Zoho Books Settings → Integrations → Zoho CRM) automatically creates Books customers from CRM contacts and vice versa, without custom API code.

What are the Zoho Books API rate limits?

Zoho Books API limits are 60 requests per minute on the free plan and 100 requests per minute on paid plans, per organization. There is also a daily limit of 2,500 API calls on free plans and 5,000 on paid plans. For dashboards that make multiple simultaneous API calls, use caching and Promise.all to stay within these limits.

How do I handle Zoho Books data for multiple countries?

Zoho Books is region-specific — each Zoho data center (US, EU, IN, AU, JP) hosts separate instances. Your API endpoint and OAuth endpoint must match the region of your Zoho account. If you build an app serving customers in multiple regions, each user's tokens point to their regional endpoint, and you must store the region alongside their credentials to construct the correct API URLs.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

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

Book a free consultation

We put the rapid in RapidDev

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