Skip to main content
Business & GrowthAccounting Software235 lines

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'

Quick Summary24 lines
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 lines
Paste into your CLAUDE.md or agent config

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

  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.

Common Pitfalls

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

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 code in response — Every Zoho response has a code field (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

Get CLI access →

Related Skills