---
name: zoho-books-oauth-app
description: The Zoho Books layer on top of the shared `zoho-api-console-oauth` skill — the Books scope families and the absence of a documented full-access scope, the `organization_id` parameter required on every single API call and the two ways to discover it, why a multi-organization customer breaks a connector that picks the first one, the country-edition model that gates whole endpoint families and cannot be changed after signup, the per-organization request and concurrency limits, and the Books / Invoice / Inventory surface split where three products share one data model but not one scope namespace. Use when asked to get Zoho Books OAuth credentials, choose Zoho Books scopes, debug a Books call that authorizes but returns nothing, or decide whether an integration belongs against Books, Invoice or Inventory. Read `zoho-api-console-oauth` first for the console and data-centre mechanics; use the Zoho CRM or Zoho Recruit skill for those products.
---

# Zoho Books OAuth2 App Registration

The client registration is the base skill's job. What is specific to Books is that **a valid token is not enough to
make a single successful call.** Every Books endpoint needs an `organization_id`, the token does not carry one, and
a customer may have several. A connector that authorizes perfectly and then reads an empty ledger has almost always
picked the wrong organization — or none.

The second Books-specific thing is that **the API surface is not the same for every customer.** Books ships as
country-specific editions, whole endpoint families exist only in some of them, and the edition is fixed when the
organization is created. And the third is that Books shares its data model with Zoho Invoice and Zoho Inventory
without sharing a scope namespace — so "we already support Books" does not mean what it sounds like.

## Built on: `zoho-api-console-oauth`

**Read `zoho-api-console-oauth` first, then this file.** It owns all the console and protocol mechanics, and this
file does not repeat them:

- The Zoho API Console, client types, and why a multi-tenant connector needs **Server-based**, not Self Client.
- **The data-centre model** — `location` / `accounts-server` on the callback, the per-DC accounts and API hosts, the
  Multi DC toggle, and the rule that **one client ID serves every DC while the client secret is per-DC by default**.
- Redirect-URI rules and the platform's per-data-center callback list.
- The generic scope grammar `service.scope.OPERATION`, and that scopes are **comma-delimited**.
- `access_type=offline` plus `prompt=consent`, and the **20-refresh-tokens-per-user-per-client** cap that silently
  revokes the oldest.
- The `Authorization: Zoho-oauthtoken {token}` header form, and taking the API host from `api_domain`.
- Capturing and rotating the secret, and limits being a per-organization concept.

If you are reading only this file, you are missing all of the above.

## Inputs to collect before you start

The base skill lists the console inputs. These are the Books-specific ones:

| Input | Notes |
| --- | --- |
| **Which Books modules must the integration reach?** | Books has no documented full-access scope — you enumerate (§1) |
| **Read-only, or read-write?** | Each module's operations are separate strings (§1) |
| **Do target customers run more than one organization?** | Decides whether "the first org" is a bug or merely a latent one (§2) |
| **Which country editions must be supported?** | Gates whole endpoint families, and cannot be changed later (§3) |
| **Is the product really Books, or Invoice or Inventory?** | Different scope namespace, different API path (§4) |
| **What plan are target customers on?** | Free plan is 1,000 requests/day for the whole org (§5) |

## Quick Start

1. Work the base skill: Server-based client, redirect URIs, Multi DC, secrets.
2. Enumerate the Books modules the integration touches and build the scope list from §1 — there is no shortcut string.
3. Include the settings scope; it is what lets you list organizations, and without it §2 has no starting point.
4. After the token exchange, **list the organizations and resolve `organization_id` deliberately** (§2).
5. Confirm the edition-gated endpoints your product needs exist for the target customers (§3).
6. Confirm the product is Books and not Invoice or Inventory — check the scope prefix and the API path (§4).
7. Size the sync against 100 requests/minute and the plan's daily cap (§5).
8. Run the base skill's round trip, plus the Books checks in §6.

## Books platform state (verified 2026-09-20 — re-verify before trusting)

- **Current API version is v3**, at `https://www.zohoapis.{domain}/books/v3/…`.
- **Eight documented data centres for Books**: `.com`, `.eu`, `.in`, `.com.au`, `.jp`, `.ca`, `.com.cn`, `.sa`.
  Note Books documents its API host for Canada as `www.zohoapis.ca` while the accounts host is `accounts.zohocloud.ca`
  — the base skill's warning about not deriving one from the other applies here directly.
- **`organization_id` is required on every request** (§2).
- **Books documents no `ZohoBooks.fullaccess.all` scope.** Zoho Invoice documents `ZohoInvoice.fullaccess.all` and
  Zoho Inventory documents `ZohoInventory.FullAccess.all`, but the Books scope table lists only the fifteen
  per-module families in §1. Do not assume the Books equivalent exists because its siblings' do.
- **A documentation defect worth knowing:** the data-centre table on the Books OAuth page lists base API URIs ending
  `/billing/` — Zoho Billing's paths, pasted into the Books docs. The correct Books base path is `/books/v3/`, as the
  same page's own example request and the Books introduction page both show. Trust the example, not the table.
- Limits are **100 requests per minute per organization** plus a daily cap by plan, plus a concurrency limit (§5).

If the Books docs do not look like this, stop and report what you actually see.

## 1. The Books scope families

Service name `ZohoBooks`; operation types `CREATE`, `READ`, `UPDATE`, `DELETE`, `ALL`. Fifteen documented families,
each mapping to a module:

| Scope name | Reaches |
| --- | --- |
| `contacts` | Customers and Vendors |
| `settings` | Items, Expense Categories, Users, Taxes, Currencies, Opening Balances — and organization listing |
| `estimates` | Quotes / Estimates |
| `invoices` | Invoices |
| `customerpayments` | Payments Received |
| `creditnotes` | Credit Notes |
| `projects` | Projects |
| `expenses` | Expenses |
| `salesorders` | Sales Orders |
| `purchaseorders` | Purchase Orders |
| `bills` | Bills |
| `debitnotes` | Vendor Credits |
| `vendorpayments` | Payments Made |
| `banking` | Banking |
| `accountants` | Accountant module — chart of accounts, journals |

So a read-only invoice integration is `ZohoBooks.invoices.READ`, and a full one is `ZohoBooks.invoices.ALL`.

Four things to get right:

- **`settings` is doing more work than its name suggests.** It is the Items module, the Users module, tax
  configuration — *and* the organizations listing that §2 depends on. Zoho documents `ZohoBooks.settings.READ` as the
  scope for `GET /organizations`. **Practically every Books integration needs it**, including ones that never touch
  a setting, because without it you cannot discover the organization you must then name on every call.
- **There is no documented full-access scope**, so the list is the list. Enumerate the modules the product touches;
  do not reach for a shortcut string that the Books docs do not define.
- **The families are coarser than CRM's.** `accountants` covers the whole accountant surface; `banking` covers
  banking. There is no per-endpoint scoping, so a connector that needs one journal read takes the accountant module
  with it. Say that plainly in a customer conversation rather than implying finer control than exists.
- **Operations are separate strings, and `ALL` includes `DELETE`.** An integration that creates and updates but must
  never delete should say so with `CREATE` and `UPDATE` rather than `ALL` — and it is worth doing in accounting
  software specifically, where a delete is not a soft delete in the customer's mind.

## 2. `organization_id` — required on every call

This is the Books fact that breaks integrations.

> *"In Zoho Books, your business is termed as an organization. If you have multiple businesses, you simply set each
> of those up as an individual organization … The parameter `organization_id` along with the organization ID should
> be sent in with every API request to identify the organization."*

Each organization is independent: its own ID, base currency, time zone, language, contacts and reports. **The OAuth
token does not identify one.** A token grants access to the organizations its user belongs to; which one a call acts
on is decided by the parameter on that call.

```
GET https://www.zohoapis.com/books/v3/invoices?organization_id=10234695
```

### How to discover it

Two documented ways, and only one of them is yours:

1. **`GET /organizations`** — returns every organization the authorizing user can reach, each with its
   `organization_id`, name, contact, currency, fiscal-year start, time zone, and an `is_default_org` flag. Requires
   `ZohoBooks.settings.READ`. **This is the connector's route**, and it is why §1 insists on the settings scope.
2. **The customer reads it out of the admin console** — organization dropdown → *Manage Organizations*. Useful when
   a human is configuring a connection by hand, useless for a self-serve flow.

### The multi-organization trap

The listing is an array, and for a great many customers — accountants, franchises, holding companies, anyone with
a second legal entity — it has more than one element.

- **Picking element zero is not a selection, it is a coin flip.** The order is not documented as meaningful, and
  `is_default_org` marks the user's default, which is not necessarily the one the customer wants synced.
- **The failure is silent and looks like a data problem.** The token is valid, the calls return `200`, and the
  ledger is simply the wrong company's — or empty, if the default org is a dormant one. Nobody sees an error.
- **It is not fixed by re-authorizing**, so customers and support both chase the wrong thing.
- **The right shape is to ask.** List the organizations after the token exchange and let the customer choose, or at
  minimum surface which one was chosen so a human can catch it. If the connection model has no place to store that
  choice, that is the gap to raise — not something to paper over with a default.
- **Store the chosen ID on the connection.** Re-deriving it per call re-runs the discovery and spends requests
  against the customer's per-minute budget (§5).

One more consequence: **the per-minute and per-day limits in §5 are per organization.** A customer with four
organizations has four separate budgets, and a connector syncing all four is not competing with itself — but it is
also making four times the calls, and each needs its own ID on every request.

## 3. Country editions and regions — two different things

**Do not conflate the data centre with the edition.** The base skill's DC model is about *where the data lives*. The
edition is about *what accounting rules the organization runs under*, and it gates endpoints.

- **The edition is chosen at organization signup and is effectively permanent.** Zoho: *"The Country, Base Currency
  and Timezone can not be changed if you are using a country specific edition."* The remedy is a new organization,
  which a customer will not do for your integration.
- **Taxes are handled differently in each edition**, and that reaches the API, not just the UI.
- **Whole endpoint families are edition-gated**, and the Books docs label them inline. As of 2026-09-20 the docs mark,
  among others: tax authorities as **US and CA edition only** (with the listing endpoint marked **US edition only**),
  and the entire tax-exemption family — create, list, update, get, delete — as **US edition only**. Separately, the
  e-invoicing families on invoices and credit notes exist for the editions whose jurisdictions mandate e-invoicing.
- **A call to an endpoint the edition does not have fails for that customer and only that customer.** It is not a
  scope problem and not a permissions problem, and it will not reproduce in your test org unless your test org is in
  the same edition.

What to do about it:

- **Ask which editions the product must support**, and check the specific endpoints you depend on against the doc
  labels before promising anything.
- **Treat tax as the high-risk surface.** It is where the editions differ most and where getting it wrong is a
  compliance problem for the customer rather than a bug for you.
- **Test in more than one edition** if the product claims more than one, or say plainly that it has only been proven
  in one.
- Both the edition and the data centre are visible to the customer in the URL they use — `books.zoho.com` versus
  `books.zoho.in` and so on — which makes "which one are you on?" an easy question to ask them.

## 4. Books vs Invoice vs Inventory

Three Zoho products share a great deal of one data model — contacts, items, estimates, invoices, payments, credit
notes — and **do not share a scope namespace or an API path**:

| Product | Scope prefix | API base path | Full-access scope |
| --- | --- | --- | --- |
| Zoho Books | `ZohoBooks.` | `/books/v3/` | Not documented |
| Zoho Invoice | `ZohoInvoice.` | `/invoice/v3/` | `ZohoInvoice.fullaccess.all` |
| Zoho Inventory | `ZohoInventory.` | `/inventory/v1/` | `ZohoInventory.FullAccess.all` |

Note the casing difference between the two full-access scopes — `fullaccess` for Invoice, `FullAccess` for
Inventory. That is Zoho's own inconsistency, reproduced here deliberately.

How to think about the split:

- **Books is the superset on the accounting side.** It adds bills, vendor payments, banking, the accountant module
  and the general ledger — surfaces Invoice does not have at all.
- **Invoice is the receivables-only subset**, and the relationship is live rather than conceptual: the Books API
  itself exposes operations to **downgrade an organization to Zoho Invoice** and **upgrade an organization to Zoho
  Books**. It is the same organization either way. So a customer can move between products under a running
  integration, and the scope prefix that worked yesterday can be the wrong one today.
- **Inventory is the stock-and-fulfilment product** — items and composite items, inventory adjustments, transfer
  orders, packages, shipment orders, purchase receives, sales returns — alongside a sales/purchase surface that
  overlaps Books.
- **A token scoped for one does not reach another.** `ZohoBooks.invoices.READ` does not read Zoho Invoice's
  invoices. If a customer says "we use Zoho for invoicing", that is not yet enough information to pick a scope.
- **They are separate connectors, not variants.** If the platform ships distinct Books, Invoice and Inventory
  connectors, a customer on the wrong one authorizes successfully and then finds the API path does not answer. Check
  the product before the scopes.

## 5. Limits are per organization

Books meters **requests**, not credits — a different model from CRM and Recruit, and easier to reason about:

| Plan | Requests per day |
| --- | --- |
| Free | 1,000 |
| Standard | 2,000 |
| Professional | 5,000 |
| Premium / Elite / Ultimate | 10,000 |

Plus, for every plan: **100 requests per minute per organization**, and a **concurrency limit** of 5 concurrent calls
(Free) or 10 (paid, described as a soft limit).

All of it returns `429`, with a code that tells you which limit you hit: `45` for the daily cap, `44` for the
per-minute cap, `1070` for concurrency. Read the code rather than treating every `429` the same — the daily cap
means stop until tomorrow, the per-minute cap means slow down, and concurrency means reduce parallelism.

Three things this implies for a connector:

- **1,000 requests a day is small.** A Free-plan customer with a few thousand invoices cannot be fully synced daily
  by a connector that pages 100 at a time and makes a follow-up call per record. Design for the Free ceiling.
- **Re-discovering `organization_id` costs requests.** Cache it (§2).
- **Multi-organization customers have multiple budgets** but also multiple syncs; the arithmetic does not cancel.

## Product fact — what the Books connector asks for

> **As of 2026-09-20, Unified.to's Zoho Books connector requests** a per-unified-object scope set rather than one
> flat list:
>
> - **`ZohoBooks.settings.READ` appears in essentially every set**, including read-only ones — consistent with §1
>   and §2, since it is what makes the organizations listing reachable.
> - **Module scopes added per object**, for example `ZohoBooks.invoices.READ`, `ZohoBooks.bills.READ`,
>   `ZohoBooks.salesorders.READ`, `ZohoBooks.purchaseorders.READ`, `ZohoBooks.contacts.READ`,
>   `ZohoBooks.expenses.READ`, `ZohoBooks.customerpayments.READ`, `ZohoBooks.accountants.READ` and
>   `ZohoBooks.banking.READ`.
> - **Write sets enumerate `CREATE`, `UPDATE` and `DELETE` explicitly** on the relevant modules rather than using
>   `.ALL` — more strings, but an honest list on a consent screen.
> - **Identity/login only:** `ZohoBooks.settings.READ`.
> - **No full-access scope is requested**, consistent with §1: Books does not document one.
>
> Two things to raise with the connector's owner rather than copy:
>
> 1. **On connect it lists the customer's organizations and remembers the first one as the connection's default.**
>    For a single-organization customer that is correct and invisible. For a multi-organization customer the default
>    is whichever company happens to come back first, and nothing on the consent screen tells the customer which
>    that was.
> 2. **That default is a fallback, not a lock.** An organization can also be named per request, or carried on the
>    object identifier itself, and those take precedence. So multi-entity access is reachable — but only for a
>    caller who knows to ask for it. Someone who never passes an organization gets the first one, silently and
>    consistently, which is the §2 trap wearing a friendlier face.
>
> Scope sets change. This note is dated, not live; confirm the current set with the connector's owner before
> submitting anything.

## 6. Books-specific end-to-end checks

On top of the base skill's authorize → refresh round trip:

1. Call `GET /organizations` and **look at how many came back**. If your test account has exactly one, that test has
   not exercised §2 — get an account with two before believing the connector is multi-entity safe.
2. Make one real read with `organization_id` on the query string, and one deliberately **without** it, to confirm
   you recognise the failure mode.
3. Confirm the organization actually synced is the one the customer expects, by name, not by ID.
4. Read one record from **every** module the integration claims — Books scopes are per module and fail per module.
5. If the product supports more than one country edition, exercise an edition-gated endpoint (§3) in the edition
   that has it and confirm you handle its absence elsewhere.
6. Check the API path is `/books/v3/` and not an Invoice or Inventory path (§4).
7. Confirm the connector caches the organization ID rather than re-deriving it per call (§2, §5).

| Symptom | Cause |
| --- | --- |
| Token valid, every call fails or returns nothing useful | `organization_id` missing from the request (§2) |
| Token valid, calls succeed, data belongs to the wrong company | Wrong organization picked from the listing (§2) |
| `GET /organizations` itself fails | `ZohoBooks.settings.READ` not requested (§1) |
| One module works, another is forbidden | Books scopes are per module — the second one was not requested (§1) |
| An endpoint works for one customer and 404s for another | Edition-gated endpoint; the second customer's edition does not have it (§3) |
| Scopes accepted, but the API path does not answer | Wrong product — Invoice or Inventory, not Books (§4) |
| `429` with code `45` | Daily request cap for that plan (§5) |
| `429` with code `44` | 100 requests per minute per organization (§5) |
| `429` with code `1070` | Concurrency limit — reduce parallel calls (§5) |
| Works in the US, fails elsewhere | Data centre, not edition — see the base skill (§3 and base §4) |

## Stop and ask

Beyond the base skill's list, hand back to a human when:

- Target customers may run **multiple organizations** and there is no customer-facing way to choose which one syncs
  (§2). Do not ship a default and call it done.
- The product must support **country editions** whose endpoint availability nobody has checked (§3) — particularly
  anything touching tax or e-invoicing.
- It is unclear whether the integration belongs against **Books, Invoice or Inventory** (§4), or a customer may move
  between them.
- A **Free-plan** customer's volume plainly exceeds 1,000 requests/day and someone wants a sync-frequency commitment
  (§5).
- Anyone proposes a delete-capable scope on accounting records without an explicit owner for that decision (§1).

## Unverified as of 2026-09-20

- **Whether a `ZohoBooks.fullaccess.all` scope is accepted in practice.** The Books scope table does not list one,
  while Invoice and Inventory document theirs. Absence from the docs is not proof of rejection — but do not build on
  it without testing, and prefer the documented per-module strings.
- **Whether the `/billing/` base URIs in the Books OAuth page's data-centre table are a doc defect or a live
  alternative.** Every other Books source, including that page's own example, uses `/books/v3/`. Treat `/books/v3/`
  as correct.

## References

Books-specific only; the base skill carries the generic Zoho OAuth references. Verified to resolve on 2026-09-20.

- **Books API introduction (`organization_id`, data centres, API call limits)** — https://www.zoho.com/books/api/v3/introduction/
- **Books OAuth (scope table, registration, token validity, Multi DC)** — https://www.zoho.com/books/api/v3/oauth/
- Organizations API (listing, upgrade/downgrade between Books and Invoice) — https://www.zoho.com/books/api/v3/organizations/
- Zoho Invoice OAuth (scope namespace, `ZohoInvoice.fullaccess.all`) — https://www.zoho.com/invoice/api/v3/oauth/
- Zoho Inventory OAuth (scope namespace, `ZohoInventory.FullAccess.all`) — https://www.zoho.com/inventory/api/v1/oauth/
- Country-specific edition (country, currency and timezone are fixed) — https://www.zoho.com/us/books/kb/general/different-country-edition.html
