Announcing CIMD support for MCP Client registration
Learn more

MCP vs API for Integrating Zoho Books to AI Agents

Zoho hosts its own MCP server for Zoho Books, through a verified Claude connector and through the Zoho MCP hub, and runs the Zoho Books API v3 across eight data centers. Both act as the signed-in user, and both meet the same twist: one bookkeeper's grant reaches every client organization they belong to. This guide compares what each path lets an accounts-receivable agent do, the token rules that break production agents, and how to keep each run inside one client's books.

TL;DR

  • Zoho hosts the Zoho Books Model Context Protocol (MCP) server itself. There are two ways in: a Zoho-built connector in Claude's directory with 49 tools and a region picker for eight data centers, and Zoho MCP, a hub where you build your own server from Zoho Books tools and connect it to ChatGPT, Cursor, or any MCP client. A Zoho MCP server URL carries an API key, and Zoho's docs say to treat it "as you would your password."
  • The Zoho Books API v3 is OAuth 2.0 through Zoho Accounts, with per-module scopes such as ZohoBooks.invoices.READ, 1-hour access tokens, and refresh tokens that never expire on their own. Every call needs an organization_id, and every call goes to the user's data center: zohoapis.com, .eu, .in, .com.au, .jp, .ca, .com.cn, or .sa.
  • Limits are per organization, not per app: 100 requests per minute, 1,000 to 10,000 calls per day by plan, and 5 or 10 concurrent calls. Zoho says MCP usage counts against the same API limits, so a chatty assistant and your nightly sync drain one budget.
  • One Zoho login can reach many organizations. A bookkeeper at an accounting firm authorizes once, and the grant covers every client organization they belong to, with organization_id as just another parameter. Whichever path you pick, pinning the organization is your job.
  • Scalekit's zohobooks connector exposes 119 prebuilt tools over the Zoho Books API (57 read, 62 write, 0 destructive) with OAuth 2.0 through your own Zoho app. Scalekit stores and refreshes each user's tokens in the token vault, so the MCP vs API decision doesn't change your auth infrastructure.

The decision your Zoho Books agent forces first

Your product is an accounts-receivable agent for bookkeeping firms. A firm's bookkeepers each look after 30 to 50 small-business clients in Zoho Books, and the agent's job is to tell them which invoices are overdue across those clients, chase payment with reminders, explain why a customer's statement doesn't match their remittance, and record payments when they land. You check what Zoho offers and find more than most vendors ship: an official, hosted MCP server, a Claude directory connector built by Zoho, a central Zoho MCP hub, and a mature REST API with eight regional endpoints. They overlap heavily on invoices and contacts. They differ on who creates the server, whose credential it runs as, which organization it touches, and whether anything runs when no one is chatting. Here's how to pick.

What Zoho Books MCP and Zoho Books API actually are

Zoho Books MCP

Zoho's MCP story has two entry points, and both are hosted by Zoho.

The first is the Zoho Books connector in Claude's directory. Zoho built it, Anthropic lists it as verified, and it was added in March 2026. You add it from Customize > Connectors in Claude, pick the region your Zoho account lives in, and sign in. The connector has a separate endpoint per data center, such as claude-zohobooks.zohomcp.com for the US, .zohomcp.eu for Europe, .zohomcp.in for India, and .zohomcp.com.au, .ca, .sa, .jp, and .ae for the rest. The listing shows 49 tools, including list_invoices, get_invoice, list_contacts, get_customer_payment, create_sales_order, create_purchase_order, update_estimate, update_tax, and five deletes: delete_contact, delete_item, delete_sales_order, delete_estimate, and delete_customer_payment. Zoho's help page notes that "the Claude default connector supports only the actions listed under Tools."

The second is Zoho MCP, a hub for building your own server across Zoho apps and third-party tools. You pick the Zoho Books tools you want, optionally combine them with Zoho CRM or Stripe tools, and Zoho generates a server URL for ChatGPT, Cursor, Windsurf, or any other MCP client. Zoho's Zoho MCP documentation is direct about what that URL is: "Your URL will contain the required secure API Key," and "Treat your MCP Server URL as you would your password!" Adding Zoho Books to a server requires OAuth 2.1. By default Zoho uses Authorization on Demand, where each person authenticates with their own Zoho account; the alternative, Authorization via Connections, lets the Super Admin share one set of OAuth tokens with every member. Zoho MCP keeps a log of every tool access for 30 days.

Zoho MCP launched to an early access waitlist in July 2025. Zoho's pricing page now describes it as "included at no additional cost for all Zoho users" with no beta label, and states that usage counts against each Zoho service's existing API limits. Zoho's launch post for Zoho Books MCP, from April 2026, adds that access "requires admin approval before users can perform any action" and that admins choose which tools are enabled.

Community servers such as ossycodes/zoho-books-mcp (57 tools) and kkeeling/zoho-mcp (30 tools, ZOHO_REFRESH_TOKEN with ZohoBooks.fullaccess.all) run locally. With an official hosted server available, they mostly show where an unmanaged Zoho refresh token ends up: in a config file on someone's laptop.

Zoho Books API

The Zoho Books API is REST, with v3 as the stable version and v4 in beta. It covers organizations, contacts and contact persons, estimates, sales orders, invoices, recurring and retainer invoices, credit notes, customer payments, purchase orders, bills, vendor credits and payments, expenses, items, taxes, currencies, chart of accounts, journals, bank accounts and transactions, projects, tasks, time entries, and users.

Two details shape every request. First, data center: the base URL is https://www.zohoapis.com/books/v3 for US accounts, and Zoho runs separate stacks at zohoapis.eu, .in, .com.au, .jp, .ca, .com.cn, and .sa. A token from one data center does nothing on another. Second, organization: every request carries organization_id, which you get from GET /organizations, and one user can belong to many.

Auth is OAuth 2.0 through Zoho Accounts. You register a server-based client in the Zoho API Console, send the user to {accounts_url}/oauth/v2/auth with response_type=code, your scopes, and access_type=offline, and exchange the code at {accounts_url}/oauth/v2/token. Requests carry Authorization: Zoho-oauthtoken <access_token>. Zoho also offers a self client for single-user scripts, which is still OAuth, just without a redirect.

Comparing them where it matters for agents

What your agent can actually do

Capability
Zoho Books MCP (Claude connector and Zoho MCP)
Zoho Books API v3
Vendor-hosted server
Yes: Zoho hosts both; per-region endpoints
Yes: Zoho-owned REST contract, per-region base URLs
Find the organizations a user can reach
Yes: Claude connector supports connecting multiple organizations
Yes
List and read invoices
Yes
Yes
Create and email invoices
Yes: Invoice creation is listed in Zoho's launch post
Yes
Chase payment
Yes: Payment reminders are listed in Zoho's launch post
Yes
Record customer payments
Limited: Read and delete payment tools are visible; the listing does not show the full set
Yes
Bills, vendor payments, purchase orders
Limited: Purchase order tools visible; bills not shown
Yes
Journals and chart of accounts
Limited: Not in the listing's visible tools
Yes
Banking
Yes: Reconciling bank transactions is listed in Zoho's launch post
Limited: No reconciliation or categorization tool in the connector
Revenue and top-customer reporting
Yes: Reporting questions are listed in Zoho's launch post
No: No report tools in the connector; aggregate invoice and payment lists yourself
Hard deletes
Yes
Limited: Scalekit has no destructive tools; it voids invoices and bills instead
React to events
No: Tools run only inside a chat
Yes: Workflow-rule webhooks with an HMAC signature header
Per-user OAuth for your own customers
Limited: Per-user OAuth inside Claude or a Zoho MCP account; your product cannot mint servers in a customer's Zoho MCP account
Yes: Authorization code flow per user through your own Zoho client

Zoho's MCP column is strong: official, hosted, regional, and covering the AR basics plus reporting the Scalekit connector doesn't wrap. The gaps are about shape, not quality. The Claude connector is a fixed tool set inside Claude. A Zoho MCP server is created by a person inside a Zoho MCP account and addressed by a URL that is itself a credential. Neither runs an AR pipeline for 300 firms on a schedule.

The delete row deserves a second look. The Claude connector hands the model delete_customer_payment. For an accounting system, deleting a recorded payment reopens the invoice and changes the ledger, and it is a tool an assistant can reach while answering "did Bowman pay last week." The Scalekit connector ships no destructive tools at all; it voids invoices and bills, which keeps them on record. Which one you want depends on who reads the model's output before it acts.

What the MCP path makes easier

"Which of my clients have invoices more than 30 days overdue, and what's the total per client" on the raw API is a chain. Call GET /organizations to list the client organizations. For each one, page through GET /invoices with filter_by=Status.OverDue and an organization_id, follow page_context.has_more_page, and sum balances in your own code. Do that against the right data center, inside a 100-requests-per-minute budget per organization, with a 1-hour token.

The Claude connector and a Zoho MCP server turn that into tools a model picks by name, with Zoho handling the region and the token. For a bookkeeper working in Claude, that is a working AR assistant in five minutes, built and maintained by the company that owns the API. Writing the schema is the hard part. Not the API call.

The tradeoff is control over the surface. On the Claude connector, the tool list is Zoho's, deletes included. On Zoho MCP, an admin curates the tools, but the result lives in Zoho's console, not in your codebase.

The auth path each one puts you on

On the MCP path, the Claude connector runs a Zoho sign-in for each Claude user, and Zoho MCP runs OAuth 2.1 per person by default through Authorization on Demand. That part is sound: actions run as the person, with their Zoho Books role, and Zoho's docs note it gives "more accurate auditing via ZUID." The weak point is the server URL. A Zoho MCP URL carries an API key, so anyone holding it can call the configured tools. It ends up in claude_desktop_config.json, a Cursor settings file, or a shared doc. Zoho lets you regenerate the key, which breaks every client using the old URL at once. And if the admin chose Authorization via Connections, the URL acts with the Super Admin's shared tokens for everyone.

On the direct API path, you run OAuth yourself and get Zoho's real least-privilege control: scopes. The format is ZohoBooks.<module>.<operation>, with operations CREATE, READ, UPDATE, DELETE, and ALL, across modules such as invoices, contacts, customerpayments, bills, banking, settings, and accountants. A reporting agent can live on ZohoBooks.settings.READ, ZohoBooks.invoices.READ, ZohoBooks.contacts.READ, and ZohoBooks.customerpayments.READ; you need ZohoBooks.settings.READ to list organizations. The tempting shortcut is ZohoBooks.fullaccess.all, which several integration guides, including Apideck's, use by default. It grants every operation in every module, deletes included.

Zoho's token rules are where production agents break:

  1. Access tokens last one hour. Refresh tokens last until the user revokes them.
  2. Refresh tokens are capped at 20 per user. Minting a 21st silently invalidates the oldest. A user who re-consents to your app repeatedly, or connects it alongside a dozen other Zoho integrations, can kill the refresh token your agent depends on.
  3. Access token minting is capped at 10 per refresh token per 10 minutes, and each refresh token can hold 15 active access tokens. Five workers that each refresh on a 401 will hit "Access Denied" during a busy run.
  4. Data center is per user. You enable Multi DC on your client in the API Console, read the accounts-server and location the authorization response returns, and send API calls to the api_domain in the token response. Skip that, and European and Indian users get "invalid client" errors while US users work fine.

Zoho Books roles apply on top of scopes. The API acts as the user, so a staff member without permission to record payments gets Zoho's code 57, "You are not authorized to perform this operation," whatever your scopes say. What the user can't do, the agent can't do.

Both paths still need per-user isolation

In a multi-tenant B2B agent, MCP gives you a credential per user and the direct API gives you a credential per user. In neither case does the path itself solve storage, rotation, or revocation; those are infrastructure problems regardless of which path you choose.

Zoho Books adds a second axis: organizations. A bookkeeper's single grant covers every client organization they belong to, and the API picks one per request through organization_id. In an agent, that parameter is filled in by the model. Ask it to "send reminders for Bowman's overdue invoices" while two clients have a customer named Bowman, and the model chooses which ledger to write to. Per-user credentials stop one bookkeeper from reaching another bookkeeper's clients. They do nothing to stop the agent from crossing between clients the same bookkeeper can see. That boundary has to be pinned in your code, per run, outside the model.

Recommended reading: access control for multi-tenant AI agents

What you own in production

On the MCP path, Zoho owns the server, the tool schemas, the region routing, and token refresh inside its connector. You own the server URL wherever it is pasted, the choice of authorization mode, and the tool selection on each Zoho MCP server. You also own the budget problem without the controls: Zoho counts MCP usage against Zoho Books API limits, so a bookkeeper asking Claude to "check every client" can spend a Standard-plan client's 2,000 daily calls before your sync runs. Nothing on the MCP path learns that a payment arrived; tools run only when someone asks. Invoice amounts, customer names, and bank balances all land in model context, which your data handling terms have to cover.

On the API path, you own schemas, pagination, retries, region routing, and the token lifecycle. Zoho's limits apply per organization:

  • 100 requests per minute
  • 1,000 calls per day on Free, 2,000 on Standard, 5,000 on Professional, and 10,000 on Premium, Elite, and Ultimate
  • 5 concurrent calls on Free and 10 on paid plans, a soft limit

Past any of them, Zoho returns 429 with code 44 for the per-minute block, 45 for the daily limit, and 1070 for concurrency. The daily limit is the one that hurts. It is shared by every integration that organization has connected, and a Standard-plan client running a payroll sync, a CRM sync, and your agent may leave you a few hundred calls. Budget per organization, not per app.

Zoho Books has no idempotency key on create endpoints. An agent that retries POST /customerpayments after a timeout can record the same payment twice, and a retried reminder emails the customer twice. Deduplication is yours: key every write on the business event, such as the bank reference or the invoice and date, and check before writing.

For events, Zoho Books uses workflow rules with webhook actions, configured in Zoho Books under Settings > Automation, not through the API. Zoho's webhook docs set the constraints: one webhook per workflow rule, a 5-second connect and 10-second read timeout, five retries by default with the same payload, and a daily webhook limit that varies by plan, with deliveries marked "Skipped" once it is reached. You can secure a webhook with a secret token, and Zoho then signs each request with an HMAC-SHA256 X-Zoho-Webhook-Signature header. Because retries resend the same payload, your handler must be idempotent. Because deliveries can be skipped, a reconciliation job behind it is not optional.

When to use MCP, when to use the API

Use Zoho Books MCP when:

  • A bookkeeper or business owner in Claude asks "list unpaid invoices over $5,000 for this client and draft a reminder," and reads every reply before anything is sent. The Claude connector does this with Zoho maintaining the tools.
  • A finance team wants Zoho Books and Zoho CRM in one assistant, such as creating an invoice from a closed deal, and an admin builds a Zoho MCP server with exactly those tools under Authorization on Demand.
  • Someone needs revenue or top-customer answers during month-end close, the reporting questions Zoho's MCP covers and the Scalekit connector does not.
  • You are prototyping an AR assistant against one test organization before building an OAuth client.

Use the Zoho Books API when:

  • Your product serves many firms or businesses, and each user needs their own Authorization Code grant with scopes you chose, such as read-only invoices for a reporting agent, revocable individually.
  • A nightly pipeline sends reminders for invoices 7, 14, and 30 days overdue, pinned to one organization per run, deduplicated, and paced to each organization's daily limit.
  • A bank-feed or payment-processor event drives zohobooks_customer_payment_create with a dedupe key, or a workflow-rule webhook triggers a re-read of the invoice it names.
  • You need writes the Claude connector doesn't show, such as journals with zohobooks_journal_create or vendor payments with zohobooks_vendor_payment_create, behind an approval step.

The credential problem that exists on both paths

Both paths hand you a Zoho credential per user. Neither hands you a vault. An AR product serving 60 bookkeeping firms with 5 bookkeepers each holds 300 Zoho Books grants, and each grant reaches the 30 to 50 client organizations that bookkeeper works on. That is 300 refresh tokens that never expire on their own, spread across up to eight data centers, each of which must be stored encrypted, kept out of logs and LLM context, refreshed without tripping the 10-per-10-minutes limit, and revoked at {accounts_url}/oauth/v2/token/revoke when a bookkeeper leaves the firm or the firm churns.

The MCP path doesn't reduce that count. It moves the credentials into Claude accounts and Zoho MCP URLs that you can't list or revoke from your product. The shortcut on the API path is one firm-owner grant with ZohoBooks.fullaccess.all per firm. Fewer secrets, larger blast radius: that one token can delete payments in every client's books, and every bookkeeper's request runs with the owner's reach.

Scalekit's zohobooks connector runs the OAuth flow per user, stores each user's tokens in the token vault, and refreshes them centrally, so the MCP vs API decision doesn't change your auth infrastructure. Your agent passes an identifier; credentials never touch the agent runtime or the LLM context.

Recommended reading: how to handle token refresh for AI agents

How to connect a Zoho Books agent with Scalekit

Scalekit's zohobooks connector is an API-path connector built by Scalekit with 119 tools: 57 read-only, 62 write, and none destructive. It covers organizations, users, contacts and contact persons, items, taxes, currencies, estimates, sales orders, invoices, recurring and retainer invoices, credit notes, customer payments, sales receipts, purchase orders, bills, vendor credits, vendor payments, expenses, chart of accounts, journals, bank accounts and transactions, projects, tasks, and time entries. Almost every tool takes an organization_id from zohobooks_organizations_list. The core set for an AR assistant:

Tool
What it does
zohobooks_organizations_list
Lists the organizations the connected user belongs to; call it first
zohobooks_user_current_get
Returns the Zoho Books user the connection is authorized as, with their role
zohobooks_contacts_list
Lists customers and vendors, filterable by contact_type and filter_by such as Invoice.OverDue
zohobooks_invoices_list
Lists invoices with status, customer, and due date filters
zohobooks_invoice_get
Returns one invoice with line items, balance due, and payment status
zohobooks_invoice_payments_list
Lists the payments recorded against one invoice
zohobooks_customer_payments_list
Lists payments received, filterable by customer, amount, and reference number
zohobooks_invoice_payment_reminder_send
Emails a payment reminder for an unpaid or overdue invoice

The connector authenticates with OAuth 2.0 through your own Zoho app; there is no Scalekit-managed Zoho app.

Prerequisites and the connection name

In the Scalekit dashboard, go to AgentKit > Connections, create a Zoho Books connection, and copy its redirect URI. In the Zoho API Console, create a server-based client, add that redirect URI, and enable Multi DC under Settings so users in every region can authorize; choose the option to use the same client secret for all data centers. Enter the client ID and client secret on the Scalekit connection. Scalekit's connector page doesn't document a default scope set or data center handling, so decide which scopes the connection requests and test one account per region you serve before launch.

The name you give the connection is the connection_name your code passes, and the string must match the dashboard exactly. This is the most common integration error. Copy SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET from Developers > API Credentials into .env. See the connector docs.

pip install scalekit-sdk-python python-dotenv langchain-openai

Authorize each user once

get_or_create_connected_account returns the bookkeeper's connected account. If it is not ACTIVE, get_authorization_link returns the Zoho consent link the bookkeeper opens once. Then zohobooks_organizations_list shows which client organizations the grant reaches. Store that list against the user; it is the allowlist every later run checks.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions CONNECTION_NAME = "zohobooks" # must match the connection name in AgentKit > Connections identifier = "firm_ledgerline_user_042" # your app's stable ID for this bookkeeper at this firm account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier, ) print("Authorize Zoho Books:", link.link) input("Press Enter after approving access in Zoho...") account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": raise RuntimeError(f"Zoho Books is {account.connected_account.status}, not ACTIVE") orgs = actions.execute_tool( tool_name="zohobooks_organizations_list", tool_input={}, connection_name=CONNECTION_NAME, identifier=identifier, ) organizations = orgs.data.get("organizations", []) if isinstance(orgs.data, dict) else [] for org in organizations: print(org["organization_id"], org["name"], org.get("plan_type")) print(orgs.execution_id)

Retrieve the Zoho Books tools this user can call

This is not a catalog load. The agent asks for the tools this bookkeeper's connected account is authorized to call, then narrows them to the seven a read-only AR review needs. Loading all 119 would put zohobooks_customer_payment_create, zohobooks_invoice_mark_void, and zohobooks_journal_create in front of a model whose job is to read balances and explain them. None of the seven below can change a ledger or email a customer.

tools = actions.langchain.get_tools( identifier=identifier, connection_names=[CONNECTION_NAME], tool_names=[ "zohobooks_organizations_list", "zohobooks_contacts_list", "zohobooks_invoices_list", "zohobooks_invoice_get", "zohobooks_invoice_payments_list", "zohobooks_customer_payments_list", "zohobooks_invoice_comments_list", ], page_size=100, ) tool_map = {t.name: t for t in tools} print(sorted(tool_map))

Run the agent loop

The loop runs until the model stops requesting tools. Every call executes with this bookkeeper's Zoho grant and role. Because that grant spans every client organization the bookkeeper belongs to, the loop pins one organization for the run and refuses any tool call that names another. The model cannot cross into a second client's books, even when two clients share a customer name.

from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from langchain_openai import ChatOpenAI ALLOWED_ORG_ID = "10234695" # the client organization this run is for, chosen by your app, not the model allowed = {o["organization_id"] for o in organizations} if ALLOWED_ORG_ID not in allowed: raise PermissionError("This bookkeeper's grant does not reach the requested organization") llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ SystemMessage( f"You review accounts receivable in Zoho Books for one client organization. " f"Always pass organization_id {ALLOWED_ORG_ID}; never use another. Use " "zohobooks_invoices_list with filter_by Status.OverDue to find overdue invoices, " "and due_date_before in yyyy-mm-dd to narrow by age. When page_context shows " "has_more_page true, request the next page. Use zohobooks_invoice_payments_list " "and zohobooks_customer_payments_list to check whether a payment was recorded " "against the wrong invoice. The organization has a daily API budget, so prefer " "list filters over reading invoices one by one. Never claim you changed anything." ), HumanMessage( "Which customers have invoices more than 30 days overdue as of 2026-11-09? Give " "totals per customer, and check whether Bowman & Co's October payment was applied " "to the right invoice." ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: tool = tool_map.get(tc["name"]) args = tc["args"] org_arg = args.get("organization_id") if tool is None: result = f"Unknown tool: {tc['name']}" elif tc["name"] != "zohobooks_organizations_list" and org_arg != ALLOWED_ORG_ID: result = f"Refused: organization_id must be {ALLOWED_ORG_ID} for this run" else: result = tool.invoke(args) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Deterministic calls with execute_tool

For pipelines, skip the model. execute_tool runs one tool with a fixed input and returns data plus an execution_id you can log. This job runs nightly per client organization. It confirms the bookkeeper's grant still reaches the organization, pages through overdue invoices, sends a reminder only on the 7th, 14th, and 30th day past due, and leaves an internal comment on each invoice so the next bookkeeper sees what the agent did. Zoho has no idempotency key, so the job keys each reminder on organization, invoice, and date, and skips anything it has already sent. A rerun after a crash does not email a customer twice.

from datetime import date REMINDER_DAYS = {7, 14, 30} AR_INBOX = "ar@ledgerline.example" def already_sent(key: str) -> bool: """Replace with a lookup in your own datastore.""" return key in SENT_REMINDERS def mark_sent(key: str) -> None: """Replace with an insert into your own datastore.""" SENT_REMINDERS.add(key) SENT_REMINDERS: set = set() def overdue_invoices(identifier: str, organization_id: str) -> list: invoices, page = [], 1 while True: result = actions.execute_tool( tool_name="zohobooks_invoices_list", tool_input={ "organization_id": organization_id, "filter_by": "Status.OverDue", "page": page, "per_page": 100, }, connection_name=CONNECTION_NAME, identifier=identifier, ) data = result.data if isinstance(result.data, dict) else {} invoices.extend(data.get("invoices", [])) page_context = data.get("page_context", {}) if isinstance(page_context, list): page_context = page_context[0] if page_context else {} if not page_context.get("has_more_page"): return invoices page += 1 def send_due_reminders(identifier: str, organization_id: str, today: date) -> int: orgs = actions.execute_tool( tool_name="zohobooks_organizations_list", tool_input={}, connection_name=CONNECTION_NAME, identifier=identifier, ) reachable = {o["organization_id"] for o in orgs.data.get("organizations", [])} if organization_id not in reachable: raise PermissionError(f"{identifier} no longer has access to organization {organization_id}") sent = 0 for invoice in overdue_invoices(identifier, organization_id): days_overdue = (today - date.fromisoformat(invoice["due_date"])).days if days_overdue not in REMINDER_DAYS: continue key = f"{organization_id}:{invoice['invoice_id']}:{today.isoformat()}" if already_sent(key): continue reminder = actions.execute_tool( tool_name="zohobooks_invoice_payment_reminder_send", tool_input={ "organization_id": organization_id, "invoice_id": invoice["invoice_id"], "cc_mail_ids": [AR_INBOX], "send_from_org_email_id": True, }, connection_name=CONNECTION_NAME, identifier=identifier, ) mark_sent(key) actions.execute_tool( tool_name="zohobooks_invoice_comment_add", tool_input={ "organization_id": organization_id, "invoice_id": invoice["invoice_id"], "description": f"AR agent sent a {days_overdue}-day reminder (run {reminder.execution_id}).", "show_comment_to_clients": False, }, connection_name=CONNECTION_NAME, identifier=identifier, ) print(reminder.execution_id, invoice["invoice_number"], days_overdue) sent += 1 return sent send_due_reminders(identifier, ALLOWED_ORG_ID, date(2026, 11, 9))

Each reminder costs two calls and the listing costs one per page, so a client with 400 overdue invoices and 30 reminders due spends roughly 64 calls of its daily budget. On a Free-plan client with 1,000 calls a day shared with its other integrations, schedule the job once, early, and never in a loop.

A workflow-rule webhook is the event-driven version. Configure one in Zoho Books that fires when a customer payment is created, secure it with a secret token, and verify the X-Zoho-Webhook-Signature header using the algorithm in Zoho's webhook docs before acting. Then treat the payload as a pointer: read the invoice ID from it and re-read the invoice with zohobooks_invoice_get under the bookkeeper's grant, keyed on the payment ID so Zoho's five retries are harmless.

Reaching the Zoho Books REST API

Anything the prebuilt tools don't cover, such as report endpoints or attachments, you can call directly, and Scalekit custom connectors cover authenticated REST calls the prebuilt tools don't. Here is the raw API from a single-user script, showing the two things every direct integration has to get right: the data center and the error codes. The refresh exchange goes to the user's accounts server, and API calls go to the api_domain that comes back.

import os import time import requests ACCOUNTS_SERVER = os.environ["ZOHO_ACCOUNTS_SERVER"] # e.g. https://accounts.zoho.eu, from the authorization response CLIENT_ID = os.environ["ZOHO_CLIENT_ID"] CLIENT_SECRET = os.environ["ZOHO_CLIENT_SECRET"] REFRESH_TOKEN = os.environ["ZOHO_REFRESH_TOKEN"] # never ship this to a browser or an LLM ORGANIZATION_ID = os.environ["ZOHO_ORGANIZATION_ID"] token = requests.post( f"{ACCOUNTS_SERVER}/oauth/v2/token", data={ "grant_type": "refresh_token", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "refresh_token": REFRESH_TOKEN, }, timeout=30, ) token.raise_for_status() body = token.json() if "access_token" not in body: raise RuntimeError(f"Zoho refused the refresh: {body}") api_domain = body["api_domain"] # e.g. https://www.zohoapis.eu headers = {"Authorization": f"Zoho-oauthtoken {body['access_token']}"} def get_books(path: str, params: dict) -> dict: for attempt in range(3): resp = requests.get( f"{api_domain}/books/v3/{path}", headers=headers, params={"organization_id": ORGANIZATION_ID, **params}, timeout=30, ) if resp.status_code != 429: resp.raise_for_status() return resp.json() code = resp.json().get("code") if code == 45: raise RuntimeError("Daily API limit reached for this organization; resume tomorrow") time.sleep(60 if code == 44 else 5 * (attempt + 1)) # 44: per-minute block, 1070: concurrency raise RuntimeError("Still rate limited after retries") overdue = get_books("invoices", {"filter_by": "Status.OverDue", "per_page": 100}) for inv in overdue.get("invoices", []): print(inv["invoice_number"], inv["customer_name"], inv["balance"], inv["due_date"])

Refreshing on every run is fine for one user. For 300 users it trips Zoho's 10-tokens-per-10-minutes limit.

Multi-tool, multi-tenant Zoho Books agents on a virtual MCP server

An AR agent rarely stops at Zoho Books. It finds overdue invoices, then tells the bookkeeper in Slack. Load all 119 zohobooks tools and the model holds zohobooks_customer_payment_create, zohobooks_invoice_mark_void, and zohobooks_journal_publish while answering "who's late this month." At roughly 200 tokens each, 119 tools is about 23,800 tokens before the agent does any work, and a model choosing between zohobooks_invoice_payments_list, zohobooks_customer_payments_list, and zohobooks_sales_receipts_list picks wrong more often with 116 others in context. The fix is not better prompting. It is surface reduction. A Scalekit virtual MCP server declares exactly which tools an agent role can see across connections, and session tokens keep each run bound to one user's connected accounts. Docs: set up a virtual MCP server.

Create the server once per agent role

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = actions.mcp.create_config( name="zohobooks-ar-digest", description="Reads overdue Zoho Books invoices and payments and posts a daily Slack digest", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="zohobooks", tools=[ "zohobooks_organizations_list", "zohobooks_invoices_list", "zohobooks_invoice_get", "zohobooks_customer_payments_list", ], ), McpConfigConnectionToolMapping( connection_name="slack", tools=["slack_send_message"], ), ], ) print(vmcp.config.id, vmcp.config.mcp_server_url)

Mint a session token per user, per run

One server definition serves every bookkeeper at every firm. Before each run, check that the user's accounts are active and mint a short-lived session token scoped to them. Any MCP client that speaks Streamable HTTP can connect; this example uses langchain-mcp-adapters (pip install langchain-mcp-adapters) and passes the token as a Bearer header.

import asyncio from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url user_id = "firm_ledgerline_user_042" accounts = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=user_id, include_auth_link=True, ) not_ready = [ a for a in accounts.connected_accounts if str(a.connected_account_status).upper() != "ACTIVE" ] for a in not_ready: print(f"{a.connection_name} is {a.connected_account_status}: {a.authentication_link}") if not_ready: raise RuntimeError("Connect the accounts above before starting this run") session_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=user_id, expiry=timedelta(minutes=30), ).token async def load_tools(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_server_url, "headers": {"Authorization": f"Bearer {session_token}"}, } } ) return await client.get_tools() mcp_tools = asyncio.run(load_tools()) print([t.name for t in mcp_tools])

The agent sees five tools instead of 119 plus everything Slack exposes. The endpoint is static; the identity is per-user. Unlike a Zoho MCP server URL, the endpoint carries no key; a run without a fresh session token gets nothing. One boundary stays with you: the session token binds the run to one bookkeeper, not to one client organization, so keep the organization_id check from the agent loop in whatever client consumes these tools, or run org-scoped work as deterministic pipelines.

Observability: every Zoho Books action tied to a user

Every execute_tool call returns an execution_id, and every execution is attributed to a connected account, which maps to one bookkeeper at one firm. When a client asks which run sent their customer three reminders in a week, or who recorded the duplicate $4,200 payment, the answer is a lookup, not an investigation. The reminder pipeline above also writes the execution_id into an internal invoice comment, so the trail is visible inside Zoho Books to anyone who opens the invoice.

Zoho's own logs complement that. Zoho MCP keeps 30 days of tool access logs, attributed by ZUID under Authorization on Demand, and Zoho Books records which user created or changed each transaction. Both stop being useful the moment a credential is shared. Under Authorization via Connections, or with one firm-owner token behind every bookkeeper, Zoho attributes every reminder, payment, and void to that one owner, and nothing anywhere says which bookkeeper asked. Shared credentials are a single-user solution. They do not survive a second user.

Recommended reading: audit trails for agent auth

Which one to build against

If your agent is a bookkeeper's or owner's assistant in Claude, ChatGPT, or Cursor, with a human reading every answer, Zoho's official MCP is a good fit: it is hosted by Zoho, routes every region, and covers reporting the Scalekit connector doesn't. Prefer Authorization on Demand, curate the tools, and guard the server URL like a password. If you are building an AR or bookkeeping product for many firms, build on the Zoho Books API with your own OAuth client, because no Zoho MCP server lets your product grant, scope, and revoke access per user across eight data centers, and no MCP path runs a reminder pipeline at 6 a.m. Either way, you hold a non-expiring Zoho refresh token per user that reaches every client organization they can open, and that's what needs production-grade infrastructure.

FAQs

Does Zoho Books have an official MCP server?

Yes, and Zoho hosts it. There is a Zoho-built connector in Claude's directory with 49 tools and a region picker, and there is Zoho MCP, a hub where an admin builds a server from chosen Zoho Books tools and connects it to ChatGPT, Cursor or any MCP client. Zoho counts MCP usage against the same Zoho Books API limits.

Why is a Zoho MCP server URL sensitive?

Zoho's docs say the server URL contains an API key and tell you to treat it as you would your password. Anyone holding it can call the configured tools, regenerating the key breaks every client at once, and under Authorization via Connections the URL acts with the Super Admin's shared tokens for every member.

How does Zoho Books OAuth work?

You register a server-based client in the Zoho API Console and request per-module scopes such as read invoices. Access tokens last one hour and refresh tokens last until revoked, but each user can hold only 20 refresh tokens and minting is capped at 10 access tokens per refresh token per 10 minutes. Calls must go to the user's data center.

Why does organization pinning matter for a Zoho Books agent?

One bookkeeper's grant covers every client organization they belong to, and each call picks one by organization ID. If the model fills that value, two clients with a customer of the same name can send a reminder or a write to the wrong ledger, so your code should pin one organization per run and refuse calls that name another.

What are the Zoho Books API limits?

Limits apply per organization: 100 requests per minute, between 1,000 and 10,000 calls per day depending on plan, and 5 or 10 concurrent calls. The daily limit is shared with every integration the organization has connected, and create endpoints have no idempotency key, so retries can record a payment or send a reminder twice.

How does Scalekit connect an AI agent to Zoho Books?

Scalekit's Zoho Books connector runs the OAuth flow per user through your own Zoho client, stores each user's tokens in a vault and refreshes them centrally. It exposes 119 tools, 57 read and 62 write with none destructive, which you narrow per agent role while your code pins the organization for each run.

Build your Zoho Books agent with Scalekit

Browse the Scalekit Zoho Books connector and the Zoho Books connector docs with the full tool list. See Scalekit pricing, or talk to us.

No items found.
On this page
Share this article
Scalekit
Find us on ProductHunt

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.