Zoho Books API v3
You are a senior developer integrating with the Zoho Books API v3. You build integrations for contacts, items, invoices, bills, bank accounts, reports, and recurring invoices using OAuth 2.0 and Zoho'
You are a senior developer integrating with the Zoho Books API v3. You build integrations for contacts, items, invoices, bills, bank accounts, reports, and recurring invoices using OAuth 2.0 and Zoho's RESTful endpoints. ## Key Points 1. **Store the user's data center domain** — Detect from the initial auth response and use the matching API domain. 2. **Use `filter_by` for efficient queries** — Zoho supports `Status.Active`, `Status.Overdue`, etc. on list endpoints. 3. **Handle approval workflows** — Check `is_approval_required` on the org settings before assuming invoices are immediately active. 4. **Batch operations** — Zoho supports bulk delete/email with comma-separated IDs: `?invoice_ids=id1,id2,id3`. 5. **Rate limits are 100 requests/minute** per org. Use `X-Rate-Limit-Remaining` header to track. - **Wrong data center URL** — A `.com` token doesn't work against `.eu` API. Always match the user's data center. - **`organization_id` required on every call** — Missing it returns confusing errors about permissions. - **Currency must match contact currency** — Invoices for a customer must use the customer's currency. - **Tax IDs vary by org and country** — Query `/settings/taxes` to get valid tax IDs. - **Ignoring pagination** — Default page size is 200 but large orgs can have thousands of contacts. Always paginate. - **Creating items inline** — Create items in the Items module first, then reference by ID in invoices. - **Not handling `code` in response** — Every Zoho response has a `code` field (0=success). Check it, don't just assume 200 means success. ## Quick Example ```bash npm install axios ```
skilldb get accounting-software-skills/Zoho Books API v3Full skill: 235 linesZoho Books API v3
You are a senior developer integrating with the Zoho Books API v3. You build integrations for contacts, items, invoices, bills, bank accounts, reports, and recurring invoices using OAuth 2.0 and Zoho's RESTful endpoints.
Core Philosophy
Organization-Scoped Everything
All API calls require an organization_id parameter. A Zoho user can belong to multiple organizations. Always resolve the correct org ID from /organizations before making calls.
Regional Data Centers
Zoho operates multiple data centers. Your API base URL depends on where the user's data resides: .com, .eu, .in, .com.au, .jp. Using the wrong domain returns auth errors.
Approval Workflows Affect API
If the org has approval workflows enabled, created invoices may go into a pending_approval status. Your integration must handle this state.
Setup
Dependencies
npm install axios
OAuth 2.0 Flow
import axios from 'axios';
// Base URL varies by data center
const ZOHO_ACCOUNTS_URL = 'https://accounts.zoho.com'; // or .eu, .in, .com.au
const ZOHO_BOOKS_URL = 'https://www.zohoapis.com/books/v3'; // or .eu, .in
async function getAuthUrl(): string {
const params = new URLSearchParams({
scope: 'ZohoBooks.fullaccess.all',
client_id: process.env.ZOHO_CLIENT_ID!,
response_type: 'code',
redirect_uri: 'https://yourapp.com/callback',
access_type: 'offline', // gets refresh_token
prompt: 'consent',
});
return `${ZOHO_ACCOUNTS_URL}/oauth/v2/auth?${params}`;
}
async function exchangeCode(code: string) {
const response = await axios.post(
`${ZOHO_ACCOUNTS_URL}/oauth/v2/token`,
new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.ZOHO_CLIENT_ID!,
client_secret: process.env.ZOHO_CLIENT_SECRET!,
redirect_uri: 'https://yourapp.com/callback',
code,
})
);
return response.data; // { access_token, refresh_token, expires_in }
}
const zohoApi = axios.create({ baseURL: ZOHO_BOOKS_URL });
zohoApi.interceptors.request.use((config) => {
config.headers.Authorization = `Zoho-oauthtoken ${getAccessToken()}`;
config.params = { ...config.params, organization_id: getOrgId() };
return config;
});
Key Techniques
1. Managing Contacts
async function createContact() {
const response = await zohoApi.post('/contacts', {
contact_name: 'Acme Corporation',
company_name: 'Acme Corporation',
contact_type: 'customer', // customer or vendor
billing_address: {
street: '123 Main St',
city: 'Austin',
state: 'Texas',
zip: '73301',
country: 'U.S.A.',
},
contact_persons: [
{
first_name: 'John',
last_name: 'Doe',
email: 'john@acme.com',
phone: '+1-555-0100',
is_primary_contact: true,
},
],
payment_terms: 30,
currency_id: 'currency-id',
});
return response.data.contact;
}
async function searchContacts(searchText: string) {
const response = await zohoApi.get('/contacts', {
params: { search_text: searchText, filter_by: 'Status.Active' },
});
return response.data.contacts;
}
2. Creating Invoices
async function createInvoice(customerId: string) {
const response = await zohoApi.post('/invoices', {
customer_id: customerId,
date: '2026-03-25',
due_date: '2026-04-25',
reference_number: 'PO-2026-042',
line_items: [
{
item_id: 'item-id',
name: 'Consulting Services',
description: 'March 2026 development work',
rate: 150.00,
quantity: 20,
tax_id: 'tax-id',
},
],
notes: 'Thank you for your business!',
terms: 'Net 30',
is_inclusive_tax: false,
});
return response.data.invoice;
}
async function sendInvoiceEmail(invoiceId: string) {
const response = await zohoApi.post(`/invoices/${invoiceId}/email`, {
to_mail_ids: ['client@acme.com'],
subject: 'Invoice from Your Company',
body: 'Please find your invoice attached.',
send_from_org_email_id: true,
});
return response.data;
}
3. Recurring Invoices
async function createRecurringInvoice(customerId: string) {
const response = await zohoApi.post('/recurringinvoices', {
customer_id: customerId,
recurrence_name: 'Monthly Retainer — Acme Corp',
recurrence_frequency: 'months',
repeat_every: 1,
start_date: '2026-04-01',
end_date: '2027-03-31',
line_items: [
{
item_id: 'item-id',
rate: 5000.00,
quantity: 1,
},
],
payment_terms: 15,
auto_send: true,
});
return response.data.recurring_invoice;
}
4. Bills (Vendor Invoices)
async function createBill(vendorId: string) {
const response = await zohoApi.post('/bills', {
vendor_id: vendorId,
bill_number: 'BILL-2026-015',
date: '2026-03-25',
due_date: '2026-04-10',
line_items: [
{
account_id: 'expense-account-id',
description: 'Cloud hosting — March',
rate: 2500.00,
quantity: 1,
},
],
});
return response.data.bill;
}
5. Reports
async function getProfitAndLoss(startDate: string, endDate: string) {
const response = await zohoApi.get('/reports/profitandloss', {
params: { from_date: startDate, to_date: endDate },
});
return response.data;
}
async function getBalanceSheet(date: string) {
const response = await zohoApi.get('/reports/balancesheet', {
params: { date },
});
return response.data;
}
async function getAgedReceivables() {
const response = await zohoApi.get('/reports/aged_receivables', {
params: { as_of_date: '2026-03-31' },
});
return response.data;
}
Best Practices
- Store the user's data center domain — Detect from the initial auth response and use the matching API domain.
- Use
filter_byfor efficient queries — Zoho supportsStatus.Active,Status.Overdue, etc. on list endpoints. - Handle approval workflows — Check
is_approval_requiredon the org settings before assuming invoices are immediately active. - Batch operations — Zoho supports bulk delete/email with comma-separated IDs:
?invoice_ids=id1,id2,id3. - Rate limits are 100 requests/minute per org. Use
X-Rate-Limit-Remainingheader to track.
Common Pitfalls
- Wrong data center URL — A
.comtoken doesn't work against.euAPI. Always match the user's data center. organization_idrequired on every call — Missing it returns confusing errors about permissions.- Currency must match contact currency — Invoices for a customer must use the customer's currency.
- Tax IDs vary by org and country — Query
/settings/taxesto get valid tax IDs.
Anti-Patterns
- Ignoring pagination — Default page size is 200 but large orgs can have thousands of contacts. Always paginate.
- Creating items inline — Create items in the Items module first, then reference by ID in invoices.
- Not handling
codein response — Every Zoho response has acodefield (0=success). Check it, don't just assume 200 means success. - Hardcoding organization_id — Users switch orgs. Always resolve dynamically.
Install this skill directly: skilldb add accounting-software-skills
Related Skills
FreeAgent API v2
You are a senior developer integrating with the FreeAgent API v2. You build integrations for UK freelancers and small businesses covering contacts, invoices, expenses, bank transactions, timeslips, an
FreshBooks API v3
You are a senior developer integrating with the FreshBooks API v3. You build integrations for client management, invoicing, expense tracking, time entries, and payments using OAuth 2.0 and FreshBooks'
KashFlow API
You are a senior developer integrating with the KashFlow API. You build integrations for UK small businesses covering customers, invoices, receipts, payments, bank accounts, and VAT returns using Kash
MYOB AccountRight API
You are a senior developer integrating with the MYOB AccountRight Live API. You build integrations for Australian/New Zealand businesses covering company files, contacts, invoices, payments, general j
Odoo Accounting XML-RPC / JSON-RPC API
You are a senior developer integrating with Odoo Accounting via its XML-RPC and JSON-RPC APIs. You build integrations for partners, invoices, payments, journal entries, reconciliation, and chart of ac
QuickBooks Online REST API v3
You are a senior developer integrating with the Intuit QuickBooks Online REST API v3. You build robust accounting integrations that create invoices, sync payments, manage customers/vendors, pull finan