Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Zoho Books

To integrate Replit with Zoho Books, create a Zoho API client in the Zoho Developer Console, complete the OAuth 2.0 authorization flow to obtain access and refresh tokens, store them in Replit Secrets, and call the Zoho Books REST API from a Node.js or Python backend. You can create invoices, track expenses, manage contacts, and automate accounting workflows. Deploy on Replit Autoscale for lightweight accounting integrations.

What you'll learn

  • How to create a Zoho OAuth client and complete the authorization flow
  • How to store and manage Zoho OAuth tokens in Replit Secrets
  • How to implement automatic token refresh for Zoho's short-lived access tokens
  • How to create invoices, manage contacts, and record expenses via the Zoho Books API
  • How to handle Zoho's multi-organization API structure for accounts with multiple businesses
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read60 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with Zoho Books, create a Zoho API client in the Zoho Developer Console, complete the OAuth 2.0 authorization flow to obtain access and refresh tokens, store them in Replit Secrets, and call the Zoho Books REST API from a Node.js or Python backend. You can create invoices, track expenses, manage contacts, and automate accounting workflows. Deploy on Replit Autoscale for lightweight accounting integrations.

Why Integrate Zoho Books with Replit?

Zoho Books is one of the most capable and affordable cloud accounting platforms, particularly popular among small to mid-sized businesses already using the broader Zoho ecosystem (Zoho CRM, Zoho Inventory, Zoho Projects). Its REST API provides full programmatic access to the core accounting functions: creating and sending invoices, recording expenses, managing customer contacts, tracking payments, and generating financial reports. Integrating Zoho Books with a Replit backend enables you to automate repetitive accounting tasks and connect your business applications directly to your accounting system.

Common automation use cases include: automatically generating invoices when orders are placed in your e-commerce system, syncing contacts between Zoho CRM and Zoho Books, creating expense records from uploaded receipts, triggering payment reminders for overdue invoices, and exporting financial summaries to dashboards. Each of these workflows can be built as a Replit backend that listens for events from other systems and calls the Zoho Books API in response.

The Zoho Books API uses OAuth 2.0 for authentication, which is more complex to set up than simple API keys but provides better security and support for multi-user access. The key challenge is handling Zoho's 1-hour access token expiry — your server must use the refresh token to obtain new access tokens automatically. This guide covers the complete OAuth setup and provides a token refresh helper that makes the token management transparent in your code. Zoho Books organizes data by 'organization' — if your account manages multiple companies, each has a separate organization ID that must be included in API requests.

Integration method

Standard API Integration

Zoho Books integrates with Replit through OAuth 2.0 authentication and direct REST API calls from your server-side backend. You register a server-based OAuth client in the Zoho Developer Console, complete the authorization flow to get access and refresh tokens, store them securely in Replit Secrets, and make authenticated requests to the Zoho Books API. Since Zoho access tokens expire after an hour, your server automatically refreshes them using the stored refresh token.

Prerequisites

  • A Zoho Books account (any paid plan — the free trial includes API access)
  • A Zoho Developer Console account at api-console.zoho.com to register your OAuth client
  • Your Zoho Books Organization ID (find it in Zoho Books under Settings > Organization Profile)
  • A Replit account with a Node.js or Python Repl created
  • Node.js with express and axios, or Python with flask and requests — install via Replit Shell

Step-by-step guide

1

Create a Zoho OAuth Client and Complete Authorization

Go to api-console.zoho.com and sign in with your Zoho account. Click 'Add Client' and choose 'Server-based Applications' as the client type — this is appropriate for a Replit backend that handles tokens server-side. Enter your client name (e.g., 'Replit Integration'), homepage URL, and a redirect URI. For the redirect URI, use https://your-repl-name.your-username.repl.co/oauth/callback — you'll create this endpoint in the next step, but you need the URL first. After creating the client, you'll see your Client ID and Client Secret — save these. Next, generate an authorization code by constructing the authorization URL: https://accounts.zoho.com/oauth/v2/auth?scope=ZohoBooks.fullaccess.all&client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&access_type=offline. Open this URL in your browser while logged into Zoho, approve the permissions, and Zoho will redirect to your callback URL with a code parameter (e.g., https://your-repl.repl.co/oauth/callback?code=AUTHORIZATION_CODE&location=us). Copy this authorization code immediately — it expires in 60 seconds. Now exchange it for tokens by making a POST request to https://accounts.zoho.com/oauth/v2/token with the code, client credentials, and grant_type=authorization_code. The response contains both an access_token (valid for 1 hour) and a refresh_token (long-lived). Store both in Replit Secrets.

exchange-token.js
1// One-time token exchange script — run this in your Replit Shell
2// node exchange-token.js
3const axios = require('axios');
4
5async function exchangeCode() {
6 const params = new URLSearchParams({
7 code: 'PASTE_YOUR_AUTHORIZATION_CODE_HERE',
8 client_id: process.env.ZOHO_CLIENT_ID,
9 client_secret: process.env.ZOHO_CLIENT_SECRET,
10 redirect_uri: process.env.ZOHO_REDIRECT_URI,
11 grant_type: 'authorization_code'
12 });
13
14 try {
15 const response = await axios.post(
16 'https://accounts.zoho.com/oauth/v2/token',
17 params.toString(),
18 { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
19 );
20 console.log('Access Token:', response.data.access_token);
21 console.log('Refresh Token:', response.data.refresh_token);
22 console.log('Save these to Replit Secrets as ZOHO_ACCESS_TOKEN and ZOHO_REFRESH_TOKEN');
23 } catch (error) {
24 console.error('Token exchange failed:', error.response?.data || error.message);
25 }
26}
27
28exchangeCode();

Pro tip: The authorization code from Zoho expires in 60 seconds. Have your exchange-token.js script ready to run immediately after you get the code from the redirect URL. If the code expires, go through the authorization URL flow again.

Expected result: You have a Zoho access token and refresh token. You've saved the authorization code, and you're ready to store the tokens in Replit Secrets.

2

Store OAuth Tokens in Replit Secrets

Open your Replit project and click the lock icon (🔒) in the left sidebar to open the Secrets panel. Add the following secrets: ZOHO_CLIENT_ID (your OAuth client ID from api-console.zoho.com), ZOHO_CLIENT_SECRET (your OAuth client secret), ZOHO_ACCESS_TOKEN (the access token from the exchange step), ZOHO_REFRESH_TOKEN (the refresh token from the exchange step), ZOHO_ORG_ID (your Zoho Books organization ID from Settings > Organization Profile), and ZOHO_REDIRECT_URI (the same redirect URI you used during OAuth setup). All these values are required for your integration to work. The access token will expire after 1 hour, so your server code must handle refreshing it automatically using the refresh token. Replit Secrets are encrypted at rest — access them in Node.js via process.env.SECRET_NAME or in Python via os.environ['SECRET_NAME']. Since the access token changes frequently, your server will need to update it — the next step shows a token refresh pattern that handles this automatically. Never put any of these values directly in your source code.

Pro tip: Zoho's API base URL varies by data center. US accounts use https://www.zohoapis.com, EU accounts use https://www.zohoapis.eu, and India accounts use https://www.zohoapis.in. Check your Zoho Books account URL to determine which data center your account is on and update the base URL accordingly.

Expected result: All six Zoho secrets (CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN, REFRESH_TOKEN, ORG_ID, REDIRECT_URI) are set in the Replit Secrets panel.

3

Build the Node.js Zoho Books API Server with Token Refresh

The biggest challenge with Zoho's API is handling token expiry — access tokens are valid for only 1 hour. Your server must detect when an API call fails due to token expiry (HTTP 401 with error code 57 or 'INVALID_TOKEN'), refresh the token using the refresh token, and retry the failed request. The pattern shown below implements an axios interceptor that handles this automatically. The interceptor catches 401 responses, calls the token refresh endpoint, updates the in-memory access token, and retries the original request. Because access tokens change frequently, they're stored in memory and only the refresh token (which rarely changes) is stored in Replit Secrets. Install the required packages by running npm install express axios in the Replit Shell. The Zoho Books API base path includes your organization ID: /books/v3/{endpoint}?organization_id={orgId}.

server.js
1const express = require('express');
2const axios = require('axios');
3
4const app = express();
5app.use(express.json());
6
7const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID;
8const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET;
9const ZOHO_ORG_ID = process.env.ZOHO_ORG_ID;
10const ZOHO_BASE_URL = 'https://www.zohoapis.com/books/v3'; // Change to .eu or .in if needed
11
12// In-memory token store (initialized from Secrets)
13let currentAccessToken = process.env.ZOHO_ACCESS_TOKEN;
14let refreshToken = process.env.ZOHO_REFRESH_TOKEN;
15
16async function refreshAccessToken() {
17 const params = new URLSearchParams({
18 refresh_token: refreshToken,
19 client_id: ZOHO_CLIENT_ID,
20 client_secret: ZOHO_CLIENT_SECRET,
21 grant_type: 'refresh_token'
22 });
23 const response = await axios.post(
24 'https://accounts.zoho.com/oauth/v2/token',
25 params.toString(),
26 { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
27 );
28 currentAccessToken = response.data.access_token;
29 console.log('Zoho access token refreshed successfully');
30 return currentAccessToken;
31}
32
33// Create axios client with auto-refresh interceptor
34const zohoClient = axios.create({ baseURL: ZOHO_BASE_URL, timeout: 15000 });
35
36zohoClient.interceptors.request.use(config => {
37 config.headers['Authorization'] = `Zoho-oauthtoken ${currentAccessToken}`;
38 config.params = { ...config.params, organization_id: ZOHO_ORG_ID };
39 return config;
40});
41
42zohoClient.interceptors.response.use(
43 response => response,
44 async error => {
45 if (error.response?.status === 401 && !error.config._retried) {
46 error.config._retried = true;
47 await refreshAccessToken();
48 error.config.headers['Authorization'] = `Zoho-oauthtoken ${currentAccessToken}`;
49 return zohoClient.request(error.config);
50 }
51 return Promise.reject(error);
52 }
53);
54
55// List invoices
56app.get('/api/invoices', async (req, res) => {
57 const { status, page = 1 } = req.query;
58 try {
59 const params = { page };
60 if (status) params.status = status;
61 const response = await zohoClient.get('/invoices', { params });
62 res.json({
63 invoices: response.data.invoices,
64 page_context: response.data.page_context
65 });
66 } catch (error) {
67 console.error('Zoho API error:', error.response?.data || error.message);
68 res.status(500).json({ error: 'Failed to fetch invoices' });
69 }
70});
71
72// Create invoice
73app.post('/api/invoices', async (req, res) => {
74 const { customer_id, line_items, due_date, reference_number } = req.body;
75 if (!customer_id || !line_items || line_items.length === 0) {
76 return res.status(400).json({ error: 'customer_id and line_items are required' });
77 }
78 try {
79 const response = await zohoClient.post('/invoices', {
80 customer_id,
81 line_items,
82 due_date,
83 reference_number
84 });
85 res.json({ invoice: response.data.invoice });
86 } catch (error) {
87 console.error('Zoho API error:', error.response?.data || error.message);
88 res.status(500).json({ error: 'Failed to create invoice' });
89 }
90});
91
92// Get contacts (customers)
93app.get('/api/contacts', async (req, res) => {
94 const { search_text, contact_type = 'customer' } = req.query;
95 try {
96 const params = { contact_type };
97 if (search_text) params.search_text = search_text;
98 const response = await zohoClient.get('/contacts', { params });
99 res.json({ contacts: response.data.contacts });
100 } catch (error) {
101 console.error('Zoho API error:', error.response?.data || error.message);
102 res.status(500).json({ error: 'Failed to fetch contacts' });
103 }
104});
105
106app.listen(3000, '0.0.0.0', () => console.log('Zoho Books server running on port 3000'));

Pro tip: The Zoho Books authorization header format is 'Zoho-oauthtoken YOUR_TOKEN' — not 'Bearer' or 'Basic'. This is a Zoho-specific format and using 'Bearer' instead will result in authentication failures.

Expected result: Your Express server starts, and GET /api/invoices returns your Zoho Books invoice list. When the access token expires, the interceptor automatically refreshes it and retries the request.

4

Build the Python Flask Alternative

For Python-based Replit projects, implement the same Zoho Books integration with Flask and requests. Python's cleaner class syntax makes it natural to encapsulate the Zoho API client with automatic token refresh in a reusable class. The class maintains the current access token in an instance variable and refreshes it whenever a 401 is encountered. This pattern is particularly useful in Python because it avoids global state while still keeping the token management centralized. Install the dependencies by running pip install flask requests in the Replit Shell. Zoho's error responses include an error_code field — check for code 57 (INVALID_TOKEN) or code 14 (token expired) when handling 401 responses to distinguish token errors from other authorization issues. The Python implementation also shows how to search for an existing contact before creating a new one — a common pattern to avoid duplicate contact records when creating invoices from external systems.

app.py
1import os
2import requests
3from flask import Flask, jsonify, request as flask_request
4
5app = Flask(__name__)
6
7class ZohoBooksClient:
8 def __init__(self):
9 self.client_id = os.environ['ZOHO_CLIENT_ID']
10 self.client_secret = os.environ['ZOHO_CLIENT_SECRET']
11 self.refresh_token = os.environ['ZOHO_REFRESH_TOKEN']
12 self.org_id = os.environ['ZOHO_ORG_ID']
13 self.access_token = os.environ['ZOHO_ACCESS_TOKEN']
14 self.base_url = 'https://www.zohoapis.com/books/v3' # Change to .eu or .in if needed
15
16 def _headers(self):
17 return {'Authorization': f'Zoho-oauthtoken {self.access_token}'}
18
19 def _params(self, extra=None):
20 p = {'organization_id': self.org_id}
21 if extra:
22 p.update(extra)
23 return p
24
25 def refresh_access_token(self):
26 resp = requests.post('https://accounts.zoho.com/oauth/v2/token', data={
27 'refresh_token': self.refresh_token,
28 'client_id': self.client_id,
29 'client_secret': self.client_secret,
30 'grant_type': 'refresh_token'
31 })
32 resp.raise_for_status()
33 self.access_token = resp.json()['access_token']
34 print('Zoho access token refreshed')
35
36 def request(self, method, path, **kwargs):
37 kwargs.setdefault('params', {})
38 kwargs['params'].update({'organization_id': self.org_id})
39 response = requests.request(
40 method, f'{self.base_url}{path}',
41 headers=self._headers(), timeout=15, **kwargs
42 )
43 if response.status_code == 401:
44 self.refresh_access_token()
45 response = requests.request(
46 method, f'{self.base_url}{path}',
47 headers=self._headers(), timeout=15, **kwargs
48 )
49 response.raise_for_status()
50 return response.json()
51
52zoho = ZohoBooksClient()
53
54@app.route('/api/invoices')
55def list_invoices():
56 try:
57 params = {'status': flask_request.args.get('status', 'sent')}
58 data = zoho.request('GET', '/invoices', params=params)
59 return jsonify({'invoices': data.get('invoices', [])})
60 except Exception as e:
61 return jsonify({'error': str(e)}), 500
62
63@app.route('/api/invoices', methods=['POST'])
64def create_invoice():
65 body = flask_request.json
66 if not body.get('customer_id') or not body.get('line_items'):
67 return jsonify({'error': 'customer_id and line_items are required'}), 400
68 try:
69 data = zoho.request('POST', '/invoices', json=body)
70 return jsonify({'invoice': data.get('invoice')})
71 except Exception as e:
72 return jsonify({'error': str(e)}), 500
73
74@app.route('/api/contacts/find-or-create', methods=['POST'])
75def find_or_create_contact():
76 body = flask_request.json
77 email = body.get('email')
78 if not email:
79 return jsonify({'error': 'email is required'}), 400
80 try:
81 # Search for existing contact
82 search = zoho.request('GET', '/contacts', params={'email': email})
83 contacts = search.get('contacts', [])
84 if contacts:
85 return jsonify({'contact': contacts[0], 'created': False})
86 # Create new contact
87 new_contact = zoho.request('POST', '/contacts', json={
88 'contact_name': body.get('name', email),
89 'email': email,
90 'contact_type': 'customer'
91 })
92 return jsonify({'contact': new_contact.get('contact'), 'created': True})
93 except Exception as e:
94 return jsonify({'error': str(e)}), 500
95
96if __name__ == '__main__':
97 app.run(host='0.0.0.0', port=3000)

Pro tip: Use the find-or-create contact pattern shown above when creating invoices from external systems. Looking up a contact by email before creating a new one prevents duplicate customer records in Zoho Books, which can cause confusion in reporting and reconciliation.

Expected result: Your Flask server starts and the /api/invoices endpoint returns your Zoho Books invoices. The client automatically refreshes the access token when it expires.

5

Create Invoices and Record Expenses

With the API client set up, you can now build more specific Zoho Books operations. Creating an invoice requires a customer ID (not email — look up the customer first or use the find-or-create pattern), an array of line items (each with item name, quantity, rate, and tax information), and optional fields like due date and reference number. Recording an expense requires the account ID for the expense category (found via GET /chartofaccounts?filter_by=AccountType.Expense), the amount, date, and optionally a vendor contact ID. Both operations return the created record with a Zoho-assigned ID that you should save in your own database for future reference (e.g., to mark an invoice as paid or to update an expense). The code below shows a complete invoice creation flow including a helper to look up item rates and tax codes from your Zoho Books product catalog.

invoices.js
1// Complete invoice creation with line items
2app.post('/api/invoices/from-order', async (req, res) => {
3 const { customerEmail, customerName, items, currency = 'USD', notes = '' } = req.body;
4
5 if (!customerEmail || !items || items.length === 0) {
6 return res.status(400).json({ error: 'customerEmail and items are required' });
7 }
8
9 try {
10 // Step 1: Find or create customer contact
11 const contactsRes = await zohoClient.get('/contacts', {
12 params: { email: customerEmail, contact_type: 'customer' }
13 });
14
15 let customerId;
16 if (contactsRes.data.contacts?.length > 0) {
17 customerId = contactsRes.data.contacts[0].contact_id;
18 } else {
19 const newContact = await zohoClient.post('/contacts', {
20 contact_name: customerName || customerEmail,
21 email: customerEmail,
22 contact_type: 'customer',
23 currency_code: currency
24 });
25 customerId = newContact.data.contact.contact_id;
26 }
27
28 // Step 2: Build line items
29 const lineItems = items.map(item => ({
30 name: item.name,
31 description: item.description || '',
32 rate: item.price,
33 quantity: item.quantity || 1,
34 unit: item.unit || 'qty'
35 }));
36
37 // Step 3: Create invoice
38 const invoiceResponse = await zohoClient.post('/invoices', {
39 customer_id: customerId,
40 currency_code: currency,
41 line_items: lineItems,
42 notes: notes,
43 payment_terms: 30, // Net 30 days
44 payment_terms_label: 'Net 30'
45 });
46
47 const invoice = invoiceResponse.data.invoice;
48 res.json({
49 success: true,
50 invoice_id: invoice.invoice_id,
51 invoice_number: invoice.invoice_number,
52 total: invoice.total,
53 due_date: invoice.due_date,
54 status: invoice.status
55 });
56 } catch (error) {
57 console.error('Invoice creation error:', error.response?.data || error.message);
58 res.status(500).json({ error: 'Failed to create invoice', details: error.response?.data });
59 }
60});

Pro tip: Zoho Books supports automatic invoice emailing — add 'send_from_org_email_id': true to your invoice creation payload to have Zoho automatically send the invoice to the customer's email address after creation.

Expected result: POST /api/invoices/from-order successfully creates a Zoho Books invoice with the correct customer and line items, returning the invoice ID and number.

Common use cases

Automated Invoice Creation from Orders

Build a Replit webhook endpoint that receives order completion events from your e-commerce platform or CRM, then automatically creates a corresponding invoice in Zoho Books using the Zoho Books API. The server maps order data (customer, line items, amounts) to Zoho Books invoice fields and sends the invoice creation request, optionally triggering automatic email delivery to the customer.

Replit Prompt

Build an invoice creation endpoint that accepts an order object with customer email, line items (name, quantity, price), and currency, creates a new invoice in Zoho Books by first finding or creating the customer contact, then creating the invoice with the line items, and returns the invoice ID and PDF URL.

Copy this prompt to try it in Replit

Expense Tracking Integration

Create a Replit backend that accepts expense submissions (from a mobile app, email parser, or Slack command) and records them in Zoho Books as expense entries. The server maps the expense data to Zoho Books expense fields — amount, category, date, description — and creates the expense record, handling currency conversion for foreign expenses automatically.

Replit Prompt

Build an expense recording endpoint that accepts an expense object with amount, currency, category, date, and description, looks up the matching expense account in Zoho Books by category name, and creates an expense entry, returning the expense ID and reference number.

Copy this prompt to try it in Replit

Accounts Receivable Dashboard Data

Build a backend API that queries Zoho Books for outstanding invoices, groups them by customer and aging bucket (0-30 days, 31-60 days, 60+ days), and returns a summary suitable for an accounts receivable dashboard. This gives your operations team real-time visibility into unpaid invoices without logging into Zoho Books directly.

Replit Prompt

Build an AR dashboard endpoint that fetches all overdue invoices from Zoho Books, groups them by days overdue into aging buckets (current, 1-30, 31-60, 61-90, 90+), calculates the total outstanding amount per bucket, and returns a summary with the top 10 customers by outstanding balance.

Copy this prompt to try it in Replit

Troubleshooting

API returns error code 57 'INVALID_TOKEN' on every request

Cause: The ZOHO_ACCESS_TOKEN in Replit Secrets has expired. Zoho access tokens are valid for only 1 hour. If your server has been running for more than an hour since the token was last refreshed, it will be invalid.

Solution: The token refresh interceptor in the code above handles this automatically for most cases. If you're getting this error on startup, it means the token stored in Secrets is already expired before the server starts. Manually refresh the token by running the refresh script, or implement a startup refresh call: call the refresh token endpoint at server startup to always start with a fresh access token.

typescript
1// Refresh token at startup
2async function initializeTokens() {
3 try {
4 await refreshAccessToken(); // Always start with a fresh token
5 console.log('Zoho tokens initialized');
6 } catch (e) {
7 console.error('Failed to initialize Zoho tokens:', e.message);
8 process.exit(1);
9 }
10}
11
12initializeTokens().then(() => {
13 app.listen(3000, '0.0.0.0', () => console.log('Server running'));
14});

API returns 'Organization not found' or data from the wrong organization

Cause: The ZOHO_ORG_ID secret is missing, incorrect, or your Zoho account has multiple organizations and the wrong one is being targeted.

Solution: Find your correct Organization ID in Zoho Books under Settings (gear icon) > Organization Profile — the Organization ID is shown on that page. If you have multiple organizations, note each one's ID. Update ZOHO_ORG_ID in Replit Secrets. The organization_id parameter must be included in every API request query string.

typescript
1// List all organizations your token has access to
2const orgsResponse = await axios.get('https://www.zohoapis.com/books/v3/organizations', {
3 headers: { Authorization: `Zoho-oauthtoken ${currentAccessToken}` }
4});
5console.log('Available organizations:', orgsResponse.data.organizations.map(o => ({ id: o.organization_id, name: o.name })));

OAuth authorization redirect fails with 'redirect_uri_mismatch' error

Cause: The redirect URI used in the authorization URL doesn't exactly match the redirect URI registered in the Zoho Developer Console. Even a trailing slash difference causes a mismatch.

Solution: Go to api-console.zoho.com, open your client settings, and verify the redirect URI matches exactly what you're using in the authorization URL and in your ZOHO_REDIRECT_URI secret. The comparison is case-sensitive and includes the full path. Update both places to match exactly.

Invoice creation returns 'Required field is missing' for line_items

Cause: The line items array in the invoice creation request is missing required fields. Zoho Books requires at minimum a name or item_id, a rate, and a quantity for each line item.

Solution: Ensure each line item object includes at minimum: name (string), rate (number), and quantity (number). Check that rate and quantity are numbers, not strings — Zoho rejects string values for numeric fields.

typescript
1// Correct line item structure
2const lineItem = {
3 name: 'Product Name', // required string
4 rate: 99.99, // required number (NOT string)
5 quantity: 1, // required number (NOT string)
6 description: 'Optional', // optional
7 unit: 'qty' // optional
8};

Best practices

  • Store all Zoho OAuth credentials in Replit Secrets (lock icon 🔒) — never hardcode client secrets, access tokens, or refresh tokens in your source files
  • Always implement automatic token refresh using an interceptor or wrapper class — Zoho access tokens expire after 1 hour and will cause all API calls to fail without refresh logic
  • Use the find-or-create contact pattern before creating invoices to prevent duplicate customer records from accumulating in Zoho Books
  • Include the organization_id query parameter on every API request — omitting it results in a 404 or returns data from the wrong organization
  • Use Zoho Books' native invoice email feature (send_from_org_email_id parameter) rather than building your own email sending for invoice delivery
  • Handle Zoho's pagination: the API returns up to 200 records per page with a page_context object indicating total pages — implement pagination for any endpoint that may return large datasets
  • Store Zoho entity IDs (invoice_id, contact_id, expense_id) returned from the API in your own database so you can reference them later without additional lookups
  • Deploy on Replit Autoscale for lightweight integration services; upgrade to Reserved VM only if your use case requires guaranteed uptime for real-time invoice processing

Alternatives

Frequently asked questions

How do I connect Replit to Zoho Books?

Create a server-based OAuth client at api-console.zoho.com, complete the OAuth authorization flow to get access and refresh tokens, and store them in Replit Secrets as ZOHO_ACCESS_TOKEN, ZOHO_REFRESH_TOKEN, ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, and ZOHO_ORG_ID. Your server code uses 'Authorization: Zoho-oauthtoken TOKEN' headers on all requests to https://www.zohoapis.com/books/v3/.

How do I handle Zoho's expiring access tokens in Replit?

Zoho access tokens expire after 1 hour. Implement an automatic refresh: when an API call returns a 401 error, use the refresh token to call https://accounts.zoho.com/oauth/v2/token with grant_type=refresh_token to get a new access token, then retry the failed request. The code examples in this guide include axios interceptors and Python request wrappers that handle this automatically.

Where do I find my Zoho Books Organization ID?

In Zoho Books, click the gear icon (Settings) in the left sidebar, then select 'Organization Profile'. Your Organization ID is displayed on this page. If you have multiple organizations, each has its own ID — you need to specify the correct one in the ZOHO_ORG_ID secret and include it as an organization_id query parameter in every API request.

Can I use Zoho Books API for free on Replit?

The Zoho Books API is available during the free trial period. After the trial, you need an active Zoho Books paid subscription to use the API. Replit itself is free for development. Check zoho.com/books/pricing for current plan pricing — Zoho Books is generally significantly cheaper than QuickBooks.

Why is my Zoho API returning data in the wrong currency or region?

Zoho's API base URL varies by data center: US accounts use https://www.zohoapis.com, EU accounts use https://www.zohoapis.eu, and India accounts use https://www.zohoapis.in. If your requests are going to the wrong data center URL, you'll get errors or wrong data. Check your Zoho Books dashboard URL to determine which data center your account is on.

How do I create an invoice in Zoho Books via the API?

POST to /books/v3/invoices with the customer_id (a Zoho contact ID, not an email), an array of line_items (each with name, rate as a number, and quantity as a number), and optional fields like due_date and currency_code. The API returns the created invoice object including the invoice_id and invoice_number. You can add 'send_from_org_email_id: true' to automatically email the invoice to the customer.

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.